mirror of
https://github.com/certd/certd.git
synced 2026-04-08 00:51:00 +08:00
Compare commits
147 Commits
client_syn
...
v0.1.21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b53fea32ce | ||
|
|
58ce899be2 | ||
|
|
f881d755b4 | ||
|
|
1a1b6f4b5e | ||
|
|
f65e523ba6 | ||
|
|
6b2ab3585f | ||
|
|
622b8f1f78 | ||
|
|
aee4e563b6 | ||
|
|
1f75657496 | ||
|
|
9fe021cd89 | ||
|
|
466d659f6e | ||
|
|
84e26381b5 | ||
|
|
469b5a5f69 | ||
|
|
ad77ebd2f9 | ||
|
|
b75543c3bc | ||
|
|
0677275742 | ||
|
|
0c3724e0ad | ||
|
|
803083d23c | ||
|
|
f4f8067a12 | ||
|
|
caa9f084d6 | ||
|
|
81407b65d1 | ||
|
|
8a24293fd7 | ||
|
|
8f1886a585 | ||
|
|
0a64e5fa67 | ||
|
|
7a70603971 | ||
|
|
0d5e00e744 | ||
|
|
91ba1433af | ||
|
|
12e56d14f2 | ||
|
|
7326119f52 | ||
|
|
136983cf14 | ||
|
|
105a1b80ae | ||
|
|
b8000ca533 | ||
|
|
c3e374e6e5 | ||
|
|
a9b6e87249 | ||
|
|
61de5422bf | ||
|
|
f96697f619 | ||
|
|
b4560d6370 | ||
|
|
a7bcde8d82 | ||
|
|
34bb4d54c2 | ||
|
|
e0116a1a03 | ||
|
|
12fec7939d | ||
|
|
ff8e02cceb | ||
|
|
8122bed97f | ||
|
|
991c3dbb76 | ||
|
|
399c23623d | ||
|
|
2232f21b48 | ||
|
|
e41c084381 | ||
|
|
520b27e0dc | ||
|
|
ace7e0247a | ||
|
|
9ae414b1c6 | ||
|
|
cb8c8186f1 | ||
|
|
82f86d9556 | ||
|
|
cfb1034450 | ||
|
|
2a07442a85 | ||
|
|
68c1eff81d | ||
|
|
baec15dfc6 | ||
|
|
6eb9817296 | ||
|
|
b9d5d33aaa | ||
|
|
560519894c | ||
|
|
9f434b0968 | ||
|
|
07066dde87 | ||
|
|
074c8f7cd0 | ||
|
|
e9df2355f4 | ||
|
|
45547d6f94 | ||
|
|
4a421d5b14 | ||
|
|
7b9825eb40 | ||
|
|
5cde165f0b | ||
|
|
305824ff1a | ||
|
|
86ddb72227 | ||
|
|
cca33478e4 | ||
|
|
a8f41d3c48 | ||
|
|
a25a15ca6e | ||
|
|
a39dac4dbd | ||
|
|
eab0c3be60 | ||
|
|
b4ee3d0dfc | ||
|
|
2f03e18c59 | ||
|
|
232cd7215e | ||
|
|
86b1e9959b | ||
|
|
fd130f86fd | ||
|
|
2669f509e1 | ||
|
|
d3619ad60f | ||
|
|
c26417d769 | ||
|
|
2942d39dfe | ||
|
|
df65b0509e | ||
|
|
fbde35483b | ||
|
|
7370f8b83b | ||
|
|
8b0ca1da2e | ||
|
|
466f2b1a02 | ||
|
|
f1d6cce88c | ||
|
|
ad7ababb4c | ||
|
|
72fa623674 | ||
|
|
e5d117c134 | ||
|
|
f8944a1331 | ||
|
|
e850855154 | ||
|
|
06eacee90c | ||
|
|
b1e100982e | ||
|
|
8f30158b00 | ||
|
|
576f7db978 | ||
|
|
137e043dfe | ||
|
|
15467fc233 | ||
|
|
eec0fcdcf1 | ||
|
|
a41dee015e | ||
|
|
d1a74713ef | ||
|
|
813e9e71d7 | ||
|
|
3d08dce26e | ||
|
|
4739d75f4a | ||
|
|
30cd62664b | ||
|
|
ae6b0fb111 | ||
|
|
9ec48f6ab8 | ||
|
|
f68565f444 | ||
|
|
ce5aae3795 | ||
|
|
bd00c09da0 | ||
|
|
44326c3abe | ||
|
|
2f3db7d982 | ||
|
|
9e4e3044b4 | ||
|
|
23bf0d07f7 | ||
|
|
6e61f8bcfb | ||
|
|
c7b28feb07 | ||
|
|
624eade9f2 | ||
|
|
d727a77289 | ||
|
|
c0c2cb328c | ||
|
|
8cc80deff8 | ||
|
|
5312c11472 | ||
|
|
f07ce6f47d | ||
|
|
259e797ea5 | ||
|
|
d77addd2dc | ||
|
|
c2420edb5a | ||
|
|
e1396bb107 | ||
|
|
b0def03790 | ||
|
|
9f371df372 | ||
|
|
986fd4b010 | ||
|
|
4cd7b02cb7 | ||
|
|
67bff28255 | ||
|
|
43e90503ca | ||
|
|
25dae3d1ec | ||
|
|
ec81572670 | ||
|
|
71f6d5769a | ||
|
|
49b96592a0 | ||
|
|
af0875ac4c | ||
|
|
48192b9002 | ||
|
|
ca289d4604 | ||
|
|
6529fd2fdb | ||
|
|
e84f5e8b0a | ||
|
|
df9f561fd3 | ||
|
|
06603759fd | ||
|
|
a4bd29e6bf | ||
|
|
458486dd6b |
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,18 +1,9 @@
|
||||
# IntelliJ project files
|
||||
.vscode/
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
/.idea/
|
||||
*/**/dist
|
||||
*/**/pnpm-lock.yaml
|
||||
*/**/stats.html
|
||||
.idea
|
||||
*.iml
|
||||
out
|
||||
gen
|
||||
node_modules/
|
||||
/test/*.private.*
|
||||
|
||||
/*.log
|
||||
@@ -26,7 +17,3 @@ gen
|
||||
/packages/ui/certd-ui/dist/
|
||||
/other
|
||||
/dev-sidecar-test
|
||||
/packages/core/certd/yarn.lock
|
||||
/packages/test
|
||||
/test/own
|
||||
/pnpm-lock.yaml
|
||||
|
||||
9
.gitmodules
vendored
Normal file
9
.gitmodules
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
[submodule "packages/deploy/server"]
|
||||
path = packages/deploy/server
|
||||
url = https://github.com/certd/certd-server.git
|
||||
[submodule "packages/deploy/client"]
|
||||
path = packages/deploy/client
|
||||
url = https://github.com/certd/certd-client.git
|
||||
[submodule "packages/issuer/node-acme-client"]
|
||||
path = packages/issuer/node-acme-client
|
||||
url = https://github.com/certd/node-acme-client.git
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 fast-crud
|
||||
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
|
||||
139
README.md
Normal file
139
README.md
Normal 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/
|
||||
|
||||
开始生成证书,先填写域名,支持将多个域名打到一个证书上
|
||||

|
||||
|
||||
配置证书详细信息
|
||||

|
||||
|
||||
配置证书部署流程
|
||||

|
||||
|
||||
配置好之后,点击导出按钮,导出一个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
BIN
doc/step1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
doc/step2.png
Normal file
BIN
doc/step2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
doc/step3.png
Normal file
BIN
doc/step3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
doc/tasks.png
Normal file
BIN
doc/tasks.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
8
lerna.json
Normal file
8
lerna.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"packages": [
|
||||
"packages/core/*",
|
||||
"packages/plugins/*",
|
||||
"packages/ui/*"
|
||||
],
|
||||
"version": "0.1.21"
|
||||
}
|
||||
22168
package-lock.json
generated
Normal file
22168
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
package.json
Normal file
17
package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "root",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"lerna": "^3.18.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "lerna bootstrap --hoist",
|
||||
"i-all": "lerna link && lerna exec npm install ",
|
||||
"init": "git submodule update --init --recursive"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash-es": "^4.17.20"
|
||||
}
|
||||
}
|
||||
17
packages/core/api/.eslintrc
Normal file
17
packages/core/api/.eslintrc
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.test.js", "*.spec.js"],
|
||||
"rules": {
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
2468
packages/core/api/package-lock.json
generated
Normal file
2468
packages/core/api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
packages/core/api/package.json
Normal file
26
packages/core/api/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@certd/api",
|
||||
"version": "0.1.17",
|
||||
"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": "4a421d5b142d453203c68ce6d1036e168ea2455b"
|
||||
}
|
||||
2
packages/core/api/src/access-provider/index.js
Normal file
2
packages/core/api/src/access-provider/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import { Registry } from '../registry/registry.js'
|
||||
export const accessProviderRegistry = new Registry()
|
||||
47
packages/core/api/src/dns-provider/abstract-dns-provider.js
Normal file
47
packages/core/api/src/dns-provider/abstract-dns-provider.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import _ from 'lodash-es'
|
||||
import logger from '../utils/util.log.js'
|
||||
import commonUtil from '../utils/util.common.js'
|
||||
export class AbstractDnsProvider {
|
||||
constructor ({ accessProviders }) {
|
||||
this.logger = logger
|
||||
this.accessProviders = commonUtil.arrayToMap(accessProviders)
|
||||
}
|
||||
|
||||
async createRecord ({ fullRecord, type, value }) {
|
||||
throw new Error('请实现 createRecord 方法')
|
||||
}
|
||||
|
||||
async removeRecord ({ fullRecord, type, value, record }) {
|
||||
throw new Error('请实现 removeRecord 方法')
|
||||
}
|
||||
|
||||
async getDomainList () {
|
||||
throw new Error('请实现 getDomainList 方法')
|
||||
}
|
||||
|
||||
async matchDomain (dnsRecord, domainPropName) {
|
||||
const list = await this.getDomainList()
|
||||
let domain = null
|
||||
for (const item of list) {
|
||||
if (_.endsWith(dnsRecord, item[domainPropName])) {
|
||||
domain = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!domain) {
|
||||
throw new Error('找不到域名,请检查域名是否正确:' + dnsRecord)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
4
packages/core/api/src/dns-provider/index.js
Normal file
4
packages/core/api/src/dns-provider/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Registry } from '../registry/registry.js'
|
||||
export { AbstractDnsProvider } from './abstract-dns-provider.js'
|
||||
|
||||
export const dnsProviderRegistry = new Registry()
|
||||
6
packages/core/api/src/index.js
Normal file
6
packages/core/api/src/index.js
Normal 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()
|
||||
81
packages/core/api/src/plugin/abstract-plugin.js
Normal file
81
packages/core/api/src/plugin/abstract-plugin.js
Normal file
@@ -0,0 +1,81 @@
|
||||
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 (options) {
|
||||
if (options == null) {
|
||||
throw new Error('插件安装失败:参数不允许为空')
|
||||
}
|
||||
const { accessProviders } = options
|
||||
this.logger = logger
|
||||
this.accessProviders = commonUtil.arrayToMap(accessProviders)
|
||||
}
|
||||
|
||||
appendTimeSuffix (name) {
|
||||
if (name == null) {
|
||||
name = 'certd'
|
||||
}
|
||||
return name + '-' + dayjs().format('YYYYMMDD-HHmmss')
|
||||
}
|
||||
|
||||
async executeFromContextFile (options = {}) {
|
||||
const { contextPath } = options
|
||||
const contextJson = fs.readFileSync(contextPath)
|
||||
const context = JSON.parse(contextJson)
|
||||
options.context = context
|
||||
await this.doExecute(options)
|
||||
fs.writeFileSync(JSON.stringify(context))
|
||||
}
|
||||
|
||||
async doExecute (options) {
|
||||
try {
|
||||
return await this.execute(options)
|
||||
} catch (e) {
|
||||
logger.error('插件执行出错:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行
|
||||
* @param options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async execute (options) {
|
||||
console.error('请实现此方法,context:', options.context)
|
||||
}
|
||||
|
||||
async doRollback (options) {
|
||||
try {
|
||||
return await this.rollback(options)
|
||||
} catch (e) {
|
||||
logger.error('插件rollback出错:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回退,用于单元测试
|
||||
* @param options
|
||||
*/
|
||||
async rollback (options) {
|
||||
console.error('请实现此方法,rollback:', options.context)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
async sleep (time) {
|
||||
await Sleep(time)
|
||||
}
|
||||
}
|
||||
3
packages/core/api/src/plugin/index.js
Normal file
3
packages/core/api/src/plugin/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Registry } from '../registry/registry.js'
|
||||
export { AbstractPlugin } from './abstract-plugin.js'
|
||||
export const pluginRegistry = new Registry()
|
||||
46
packages/core/api/src/registry/registry.js
Normal file
46
packages/core/api/src/registry/registry.js
Normal 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
|
||||
}
|
||||
}
|
||||
33
packages/core/api/src/store/store.js
Normal file
33
packages/core/api/src/store/store.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export class Store {
|
||||
set (key, value) {
|
||||
|
||||
}
|
||||
|
||||
get (key) {
|
||||
|
||||
}
|
||||
|
||||
buildKey (...keyItem) {
|
||||
|
||||
}
|
||||
|
||||
linkExists (linkPath) {
|
||||
|
||||
}
|
||||
|
||||
link (targetPath, linkPath) {
|
||||
|
||||
}
|
||||
|
||||
unlink (linkPath) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 全路径
|
||||
* @param key
|
||||
*/
|
||||
getActualKey (key) {
|
||||
// return 前缀+key
|
||||
}
|
||||
}
|
||||
8
packages/core/api/src/utils/index.js
Normal file
8
packages/core/api/src/utils/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
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, common
|
||||
}
|
||||
33
packages/core/api/src/utils/util.common.js
Normal file
33
packages/core/api/src/utils/util.common.js
Normal 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/api/src/utils/util.log.js
Normal file
7
packages/core/api/src/utils/util.log.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import log4js from 'log4js'
|
||||
log4js.configure({
|
||||
appenders: { std: { type: 'stdout' } },
|
||||
categories: { default: { appenders: ['std'], level: 'info' } }
|
||||
})
|
||||
const logger = log4js.getLogger('certd')
|
||||
export default logger
|
||||
9
packages/core/api/src/utils/util.path.js
Normal file
9
packages/core/api/src/utils/util.path.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import path from 'path'
|
||||
|
||||
function getUserBasePath () {
|
||||
const userHome = process.env.USERPROFILE || process.env.HOME
|
||||
return path.resolve(userHome, './.certd')
|
||||
}
|
||||
export default {
|
||||
getUserBasePath
|
||||
}
|
||||
57
packages/core/api/src/utils/util.request.js
Normal file
57
packages/core/api/src/utils/util.request.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import axios from 'axios'
|
||||
import qs from 'qs'
|
||||
import logger from './util.log.js'
|
||||
/**
|
||||
* @description 创建请求实例
|
||||
*/
|
||||
function createService () {
|
||||
// 创建一个 axios 实例
|
||||
const service = axios.create()
|
||||
// 请求拦截
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
if (config.formData) {
|
||||
config.data = qs.stringify(config.formData, {
|
||||
arrayFormat: 'indices',
|
||||
allowDots: true
|
||||
}) // 序列化请求参数
|
||||
delete config.formData
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
// 发送失败
|
||||
logger.error(error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
// 响应拦截
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
logger.info('http response:', JSON.stringify(response.data))
|
||||
return response.data
|
||||
},
|
||||
error => {
|
||||
// const status = _.get(error, 'response.status')
|
||||
// switch (status) {
|
||||
// case 400: error.message = '请求错误'; break
|
||||
// case 401: error.message = '未授权,请登录'; break
|
||||
// case 403: error.message = '拒绝访问'; break
|
||||
// case 404: error.message = `请求地址出错: ${error.response.config.url}`; break
|
||||
// case 408: error.message = '请求超时'; break
|
||||
// case 500: error.message = '服务器内部错误'; break
|
||||
// case 501: error.message = '服务未实现'; break
|
||||
// case 502: error.message = '网关错误'; break
|
||||
// case 503: error.message = '服务不可用'; break
|
||||
// case 504: error.message = '网关超时'; break
|
||||
// case 505: error.message = 'HTTP版本不受支持'; break
|
||||
// default: break
|
||||
// }
|
||||
logger.error('请求出错:', error.response.config.url, error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
return service
|
||||
}
|
||||
|
||||
export const request = createService()
|
||||
7
packages/core/api/src/utils/util.sleep.js
Normal file
7
packages/core/api/src/utils/util.sleep.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function (timeout) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve()
|
||||
}, timeout)
|
||||
})
|
||||
}
|
||||
14
packages/core/certd/.eslintrc
Normal file
14
packages/core/certd/.eslintrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.test.js", "*.spec.js"],
|
||||
"rules": {
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
2493
packages/core/certd/package-lock.json
generated
Normal file
2493
packages/core/certd/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
packages/core/certd/package.json
Normal file
29
packages/core/certd/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@certd/certd",
|
||||
"version": "0.1.19",
|
||||
"description": "a ssl cert keeper",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \\\"Error: no test specified\\\" && exit 1"
|
||||
},
|
||||
"type": "module",
|
||||
"author": "Greper",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^0.1.8",
|
||||
"@certd/api": "^0.1.17",
|
||||
"dayjs": "^1.9.7",
|
||||
"lodash-es": "^4.17.20",
|
||||
"node-forge": "^0.10.0"
|
||||
},
|
||||
"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": "4a421d5b142d453203c68ce6d1036e168ea2455b"
|
||||
}
|
||||
199
packages/core/certd/src/acme.js
Normal file
199
packages/core/certd/src/acme.js
Normal file
@@ -0,0 +1,199 @@
|
||||
import acme from '@certd/acme-client'
|
||||
import _ from 'lodash-es'
|
||||
import { util } from '@certd/api'
|
||||
const logger = util.logger
|
||||
export class AcmeService {
|
||||
constructor (store) {
|
||||
this.store = store
|
||||
}
|
||||
|
||||
async getAccountConfig (email) {
|
||||
let conf = this.store.get(this.buildAccountPath(email))
|
||||
if (conf == null) {
|
||||
conf = {}
|
||||
} else {
|
||||
conf = JSON.parse(conf)
|
||||
}
|
||||
return conf
|
||||
}
|
||||
|
||||
buildAccountPath (email) {
|
||||
return this.store.buildKey(email, 'account.json')
|
||||
}
|
||||
|
||||
saveAccountConfig (email, conf) {
|
||||
this.store.set(this.buildAccountPath(email), JSON.stringify(conf))
|
||||
}
|
||||
|
||||
async getAcmeClient (email, isTest) {
|
||||
const conf = await this.getAccountConfig(email)
|
||||
if (conf.key == null) {
|
||||
conf.key = await this.createNewKey()
|
||||
this.saveAccountConfig(email, conf)
|
||||
}
|
||||
if (isTest == null) {
|
||||
isTest = process.env.CERTD_MODE === 'test'
|
||||
}
|
||||
const client = new acme.Client({
|
||||
directoryUrl: isTest ? acme.directory.letsencrypt.staging : acme.directory.letsencrypt.production,
|
||||
accountKey: conf.key,
|
||||
accountUrl: conf.accountUrl,
|
||||
backoffAttempts: 20,
|
||||
backoffMin: 5000,
|
||||
backoffMax: 10000
|
||||
})
|
||||
|
||||
if (conf.accountUrl == null) {
|
||||
const accountPayload = { termsOfServiceAgreed: true, contact: [`mailto:${email}`] }
|
||||
await client.createAccount(accountPayload)
|
||||
conf.accountUrl = client.getAccountUrl()
|
||||
this.saveAccountConfig(email, conf)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
async createNewKey () {
|
||||
const key = await acme.forge.createPrivateKey()
|
||||
return key.toString()
|
||||
}
|
||||
|
||||
async challengeCreateFn (authz, challenge, keyAuthorization, dnsProvider) {
|
||||
logger.info('Triggered challengeCreateFn()')
|
||||
|
||||
/* http-01 */
|
||||
if (challenge.type === 'http-01') {
|
||||
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`
|
||||
const fileContents = keyAuthorization
|
||||
|
||||
logger.info(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`)
|
||||
|
||||
/* Replace this */
|
||||
logger.info(`Would write "${fileContents}" to path "${filePath}"`)
|
||||
// await fs.writeFileAsync(filePath, fileContents);
|
||||
} else if (challenge.type === 'dns-01') {
|
||||
/* dns-01 */
|
||||
const dnsRecord = `_acme-challenge.${authz.identifier.value}`
|
||||
const recordValue = keyAuthorization
|
||||
|
||||
logger.info(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`)
|
||||
|
||||
/* Replace this */
|
||||
logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`)
|
||||
|
||||
return await dnsProvider.createRecord({
|
||||
fullRecord: dnsRecord,
|
||||
type: 'TXT',
|
||||
value: recordValue
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function used to remove an ACME challenge response
|
||||
*
|
||||
* @param {object} authz Authorization object
|
||||
* @param {object} challenge Selected challenge
|
||||
* @param {string} keyAuthorization Authorization key
|
||||
* @param recordItem challengeCreateFn create record item
|
||||
* @param dnsProvider dnsProvider
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async challengeRemoveFn (authz, challenge, keyAuthorization, recordItem, dnsProvider) {
|
||||
logger.info('Triggered challengeRemoveFn()')
|
||||
|
||||
/* http-01 */
|
||||
if (challenge.type === 'http-01') {
|
||||
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`
|
||||
|
||||
logger.info(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`)
|
||||
|
||||
/* Replace this */
|
||||
logger.info(`Would remove file on path "${filePath}"`)
|
||||
// await fs.unlinkAsync(filePath);
|
||||
} else if (challenge.type === 'dns-01') {
|
||||
const dnsRecord = `_acme-challenge.${authz.identifier.value}`
|
||||
const recordValue = keyAuthorization
|
||||
|
||||
logger.info(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`)
|
||||
|
||||
/* Replace this */
|
||||
logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`)
|
||||
await dnsProvider.removeRecord({
|
||||
fullRecord: dnsRecord,
|
||||
type: 'TXT',
|
||||
value: keyAuthorization,
|
||||
record: recordItem
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async order ({ email, domains, dnsProvider, dnsProviderCreator, csrInfo, isTest }) {
|
||||
const client = await this.getAcmeClient(email, isTest)
|
||||
|
||||
let accountUrl
|
||||
try {
|
||||
accountUrl = client.getAccountUrl()
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
/* Create CSR */
|
||||
const { commonName, altNames } = this.buildCommonNameByDomains(domains)
|
||||
|
||||
const [key, csr] = await acme.forge.createCsr({
|
||||
commonName,
|
||||
...csrInfo,
|
||||
altNames
|
||||
})
|
||||
if (dnsProvider == null && dnsProviderCreator) {
|
||||
dnsProvider = await dnsProviderCreator()
|
||||
}
|
||||
if (dnsProvider == null) {
|
||||
throw new Error('dnsProvider 不能为空')
|
||||
}
|
||||
/* 自动申请证书 */
|
||||
const crt = await client.auto({
|
||||
csr,
|
||||
email: email,
|
||||
termsOfServiceAgreed: true,
|
||||
challengePriority: ['dns-01'],
|
||||
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
|
||||
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider)
|
||||
},
|
||||
challengeRemoveFn: async (authz, challenge, keyAuthorization, recordItem) => {
|
||||
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider)
|
||||
}
|
||||
})
|
||||
|
||||
// 保存账号url
|
||||
if (!accountUrl) {
|
||||
try {
|
||||
accountUrl = client.getAccountUrl()
|
||||
this.setAccountUrl(email, accountUrl)
|
||||
} catch (e) {
|
||||
logger.warn('保存accountUrl出错', e)
|
||||
}
|
||||
}
|
||||
/* Done */
|
||||
logger.debug(`CSR:\n${csr.toString()}`)
|
||||
logger.debug(`Certificate:\n${crt.toString()}`)
|
||||
logger.info('证书申请成功')
|
||||
return { key, crt, csr }
|
||||
}
|
||||
|
||||
buildCommonNameByDomains (domains) {
|
||||
if (typeof domains === 'string') {
|
||||
domains = domains.split(',')
|
||||
}
|
||||
if (domains.length === 0) {
|
||||
throw new Error('domain can not be empty')
|
||||
}
|
||||
const ret = {
|
||||
commonName: domains[0]
|
||||
}
|
||||
if (domains.length > 1) {
|
||||
ret.altNames = _.slice(domains, 1)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
131
packages/core/certd/src/index.js
Normal file
131
packages/core/certd/src/index.js
Normal file
@@ -0,0 +1,131 @@
|
||||
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 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
|
||||
|
||||
if (!(options.store instanceof Store)) {
|
||||
this.store = new FileStore(options.store || {})
|
||||
}
|
||||
this.certStore = new CertStore({
|
||||
store: this.store,
|
||||
email: options.cert.email,
|
||||
domains: this.domains
|
||||
})
|
||||
this.acme = new AcmeService(this.store)
|
||||
}
|
||||
|
||||
async certApply () {
|
||||
let oldCert
|
||||
try {
|
||||
oldCert = await this.readCurrentCert()
|
||||
} catch (e) {
|
||||
logger.warn('读取cert失败:', e)
|
||||
}
|
||||
|
||||
if (oldCert == null) {
|
||||
logger.info('还未申请过,准备申请新证书')
|
||||
} else {
|
||||
const ret = this.isWillExpire(oldCert.expires, this.options.cert.renewDays)
|
||||
if (!ret.isWillExpire) {
|
||||
logger.info('证书还未过期:', oldCert.expires, ',剩余', ret.leftDays, '天')
|
||||
if (this.options.args.forceCert) {
|
||||
logger.info('准备强制更新证书')
|
||||
} else {
|
||||
logger.info('暂不更新证书')
|
||||
|
||||
oldCert.isNew = false
|
||||
return oldCert
|
||||
}
|
||||
} else {
|
||||
logger.info('即将过期,准备更新证书')
|
||||
}
|
||||
}
|
||||
|
||||
// 执行证书申请步骤
|
||||
return await this.doCertApply()
|
||||
}
|
||||
|
||||
async doCertApply () {
|
||||
const options = this.options
|
||||
const dnsProvider = this.createDnsProvider(options)
|
||||
const cert = await this.acme.order({
|
||||
email: options.cert.email,
|
||||
domains: options.cert.domains,
|
||||
dnsProvider,
|
||||
csrInfo: options.cert.csrInfo,
|
||||
isTest: options.args.test
|
||||
})
|
||||
|
||||
await this.writeCert(cert)
|
||||
const certRet = await this.readCurrentCert()
|
||||
certRet.isNew = true
|
||||
return certRet
|
||||
}
|
||||
|
||||
createDnsProvider (options) {
|
||||
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.currentMarkPath)
|
||||
}
|
||||
}
|
||||
|
||||
async readCurrentCert () {
|
||||
const cert = await this.certStore.readCert()
|
||||
if (cert == null) {
|
||||
return null
|
||||
}
|
||||
const { detail, expires } = this.getCrtDetail(cert.crt)
|
||||
const domain = this.certStore.getMainDomain(this.options.cert.domains)
|
||||
return {
|
||||
...cert, detail, expires, domain, domains: this.domains, email: this.email
|
||||
}
|
||||
}
|
||||
|
||||
getCrtDetail (crt) {
|
||||
const pki = forge.pki
|
||||
const detail = pki.certificateFromPem(crt.toString())
|
||||
const expires = detail.validity.notAfter
|
||||
return { detail, expires }
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否过期,默认提前20天
|
||||
* @param expires
|
||||
* @param maxDays
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isWillExpire (expires, maxDays = 20) {
|
||||
if (expires == null) {
|
||||
throw new Error('过期时间不能为空')
|
||||
}
|
||||
// 检查有效期
|
||||
const leftDays = dayjs(expires).diff(dayjs(), 'day')
|
||||
return {
|
||||
isWillExpire: leftDays < maxDays,
|
||||
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 })
|
||||
}
|
||||
}
|
||||
127
packages/core/certd/src/store/cert-store.js
Normal file
127
packages/core/certd/src/store/cert-store.js
Normal file
@@ -0,0 +1,127 @@
|
||||
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, domains }) {
|
||||
this.store = store
|
||||
this.email = email
|
||||
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.currentMarkPath = this.store.buildKey(this.certsRootPath, this.domainDir, 'current.json')
|
||||
}
|
||||
|
||||
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.domainDir, dir)
|
||||
}
|
||||
|
||||
formatCert (pem) {
|
||||
pem = pem.replace(/\r/g, '')
|
||||
pem = pem.replace(/\n\n/g, '\n')
|
||||
pem = pem.replace(/\n$/g, '')
|
||||
return pem
|
||||
}
|
||||
|
||||
async writeCert (cert) {
|
||||
const newDir = this.buildNewCertRootPath()
|
||||
|
||||
const crtKey = this.buildKey(newDir, this.safetyDomain + '.crt')
|
||||
const priKey = this.buildKey(newDir, this.safetyDomain + '.key')
|
||||
const csrKey = this.buildKey(newDir, this.safetyDomain + '.csr')
|
||||
await this.store.set(crtKey, this.formatCert(cert.crt.toString()))
|
||||
await this.store.set(priKey, this.formatCert(cert.key.toString()))
|
||||
await this.store.set(csrKey, cert.csr.toString())
|
||||
|
||||
await this.store.set(this.currentMarkPath, JSON.stringify({ latest: newDir }))
|
||||
|
||||
return newDir
|
||||
}
|
||||
|
||||
async readCert (dir) {
|
||||
if (dir == null) {
|
||||
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')
|
||||
const crt = await this.store.get(crtKey)
|
||||
if (crt == null) {
|
||||
return null
|
||||
}
|
||||
const key = await this.store.get(priKey)
|
||||
const csr = await this.store.get(csrKey)
|
||||
|
||||
return {
|
||||
crt: this.formatCert(crt),
|
||||
key: this.formatCert(key),
|
||||
csr,
|
||||
crtPath: this.store.getActualKey(crtKey),
|
||||
keyPath: this.store.getActualKey(priKey),
|
||||
certDir: this.store.getActualKey(dir)
|
||||
}
|
||||
}
|
||||
|
||||
buildKey (...keyItem) {
|
||||
return this.store.buildKey(...keyItem)
|
||||
}
|
||||
|
||||
getSafetyDomain (domain) {
|
||||
return domain.replace(/\*/g, '_')
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
async setCurrentFile (file, value) {
|
||||
const currentDir = await this.getCurrentDir()
|
||||
const key = this.buildKey(currentDir, file)
|
||||
return this.store.set(key, value)
|
||||
}
|
||||
}
|
||||
66
packages/core/certd/src/store/file-store.js
Normal file
66
packages/core/certd/src/store/file-store.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Store, util } from '@certd/api'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
const logger = util.logger
|
||||
export class FileStore extends Store {
|
||||
constructor (opts) {
|
||||
super()
|
||||
if (opts.rootDir != null) {
|
||||
this.rootDir = opts.rootDir
|
||||
} else {
|
||||
this.rootDir = util.path.getUserBasePath()
|
||||
}
|
||||
if (opts.test) {
|
||||
this.rootDir = path.join(this.rootDir, '/test/')
|
||||
}
|
||||
}
|
||||
|
||||
getActualKey (key) {
|
||||
// return 前缀+key
|
||||
return this.getPathByKey(key)
|
||||
}
|
||||
|
||||
buildKey (...keyItem) {
|
||||
return path.join(...keyItem)
|
||||
}
|
||||
|
||||
getPathByKey (key) {
|
||||
return path.join(this.rootDir, key)
|
||||
}
|
||||
|
||||
set (key, value) {
|
||||
const filePath = this.getPathByKey(key)
|
||||
const dir = path.dirname(filePath)
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
fs.writeFileSync(filePath, value)
|
||||
return filePath
|
||||
}
|
||||
|
||||
get (key) {
|
||||
const filePath = this.getPathByKey(key)
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null
|
||||
}
|
||||
return fs.readFileSync(filePath).toString()
|
||||
}
|
||||
|
||||
link (targetPath, linkPath) {
|
||||
targetPath = this.getPathByKey(targetPath)
|
||||
linkPath = this.getPathByKey(linkPath)
|
||||
if (fs.existsSync(linkPath)) {
|
||||
try {
|
||||
fs.unlinkSync(linkPath)
|
||||
} catch (e) {
|
||||
logger.error('unlink error:', e)
|
||||
}
|
||||
}
|
||||
fs.symlinkSync(targetPath, linkPath, 'dir')
|
||||
}
|
||||
|
||||
unlink (linkPath) {
|
||||
linkPath = this.getPathByKey(linkPath)
|
||||
fs.unlinkSync(linkPath)
|
||||
}
|
||||
}
|
||||
88
packages/core/certd/test/index.test.js
Normal file
88
packages/core/certd/test/index.test.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import chai from 'chai'
|
||||
import { Certd } from '../src/index.js'
|
||||
import { createOptions } from '../../../../test/options.js'
|
||||
const { expect } = chai
|
||||
const fakeCrt = `-----BEGIN CERTIFICATE-----
|
||||
MIIFSTCCBDGgAwIBAgITAPoZZk/LhVIyXoic2NnJyxubezANBgkqhkiG9w0BAQsF
|
||||
ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0yMDEyMTQx
|
||||
NjA1NTFaFw0yMTAzMTQxNjA1NTFaMBsxGTAXBgNVBAMMECouZG9jbWlycm9yLmNs
|
||||
dWIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC75tGrYjly+RpcZehQ
|
||||
my1EpaXElT4L60pINKV2YDKnBrcSSo1c6rO7nFh12eC/ju4WwYUep0RVmBDF8xD0
|
||||
I1Sd1uuDTQWP0UT1X9yqdXtjvxpUqoCHAzG633f3sJRFul7mDLuC9tRCuae9o7qP
|
||||
EZ827XOmjBR35dso9I2GEE4828J3YE3tSKtobZlM+30jozLEcsO0PTyM5mq5PPjP
|
||||
VI3fGLcEaBmLZf5ixz4XkcY9IAhyAMYf03cT2wRoYPBaDdXblgCYL6sFtIMbzl3M
|
||||
Di94PB8NyoNSsC2nmBdWi54wFOgBvY/4ljsX/q7X3EqlSvcA0/M6/c/J9kJ3eupv
|
||||
jV8nAgMBAAGjggJ9MIICeTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB
|
||||
BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFAkdTjSCV3KD
|
||||
x28sf98MrwVfyFYgMB8GA1UdIwQYMBaAFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHcG
|
||||
CCsGAQUFBwEBBGswaTAyBggrBgEFBQcwAYYmaHR0cDovL29jc3Auc3RnLWludC14
|
||||
MS5sZXRzZW5jcnlwdC5vcmcwMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0Zy1p
|
||||
bnQteDEubGV0c2VuY3J5cHQub3JnLzArBgNVHREEJDAighAqLmRvY21pcnJvci5j
|
||||
bHVigg5kb2NtaXJyb3IuY2x1YjBMBgNVHSAERTBDMAgGBmeBDAECATA3BgsrBgEE
|
||||
AYLfEwEBATAoMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNyeXB0Lm9y
|
||||
ZzCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB1ABboacHRlerXw/iXGuPwdgH3jOG2
|
||||
nTGoUhi2g38xqBUIAAABdmI3LM4AAAQDAEYwRAIgaiNqXSEq+sxp8eqlJXp/KFdO
|
||||
so5mT50MoRsLF8Inu0ACIDP46+ekng7I0BlmyIPmbqFcZgnZFVWLLCdLYijhVyOL
|
||||
AHcA3Zk0/KXnJIDJVmh9gTSZCEmySfe1adjHvKs/XMHzbmQAAAF2YjcuxwAABAMA
|
||||
SDBGAiEAxpeB8/w4YkHZ62nH20h128VtuTSmYDCnF7EK2fQyeZYCIQDbJlF2wehZ
|
||||
sF1BeE7qnYYqCTP0dYIrQ9HWtBa/MbGOKTANBgkqhkiG9w0BAQsFAAOCAQEAL2di
|
||||
HKh6XcZtGk0BFxJa51sCZ3MLu9+Zy90kCRD4ooP5x932WxVM25+LBRd+xSzx+TRL
|
||||
UVrlKp9GdMYX1JXL4Vf2NwzuFO3snPDe/qizD/3+D6yo8eKJ/LD82t5kLWAD2rto
|
||||
YfVSTKwfNIBBJwHUnjviBPJmheHHCKmz8Ct6/6QxFAeta9TAMn0sFeVCQnmAq7HL
|
||||
jrunq0tNHR/EKG0ITPLf+6P7MxbmpYNnq918766l0tKsW8oo8ZSGEwKU2LMaSiAa
|
||||
hasyl/2gMnYXjtKOjDcnR8oLpbrOg0qpVbynmJin1HP835oHPPAZ1gLsqYTTizNz
|
||||
AHxTaXliTVvS83dogw==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
|
||||
GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
|
||||
MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
|
||||
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
|
||||
8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
|
||||
oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
|
||||
ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
|
||||
xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
|
||||
dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
|
||||
AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
|
||||
HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
|
||||
BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
|
||||
b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
|
||||
Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
|
||||
hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
|
||||
UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
|
||||
AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
|
||||
DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
|
||||
IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
|
||||
zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
|
||||
PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
|
||||
SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
|
||||
2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
|
||||
WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
|
||||
n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
|
||||
-----END CERTIFICATE-----`
|
||||
describe('Certd', function () {
|
||||
it('#buildCertDir', function () {
|
||||
const options = createOptions()
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.docmirror.club']
|
||||
const certd = new Certd(options)
|
||||
const currentRootPath = certd.certStore.currentMarkPath
|
||||
console.log('rootDir', currentRootPath)
|
||||
expect(currentRootPath).match(/xiaojunnuo@qq.com\\certs\\_.docmirror.club-\w*\\current.json/)
|
||||
})
|
||||
it('#writeAndReadCert', async function () {
|
||||
const options = createOptions()
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.domain.cn']
|
||||
const certd = new Certd(options)
|
||||
await certd.writeCert({ csr: 'csr', crt: fakeCrt, key: 'bbb' })
|
||||
|
||||
const cert = await certd.readCurrentCert()
|
||||
expect(cert).to.be.ok
|
||||
expect(cert.crt).ok
|
||||
expect(cert.key).to.be.ok
|
||||
expect(cert.detail).to.be.ok
|
||||
expect(cert.expires).to.be.ok
|
||||
console.log('cert:', JSON.stringify(cert))
|
||||
})
|
||||
})
|
||||
14
packages/core/executor/.eslintrc
Normal file
14
packages/core/executor/.eslintrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.test.js", "*.spec.js"],
|
||||
"rules": {
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
5696
packages/core/executor/package-lock.json
generated
Normal file
5696
packages/core/executor/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
packages/core/executor/package.json
Normal file
39
packages/core/executor/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@certd/executor",
|
||||
"version": "0.1.21",
|
||||
"description": "",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"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.17",
|
||||
"@certd/certd": "^0.1.19",
|
||||
"dayjs": "^1.9.7",
|
||||
"lodash-es": "^4.17.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/plugin-aliyun": "^0.1.19",
|
||||
"@certd/plugin-host": "^0.1.21",
|
||||
"@certd/plugin-tencent": "^0.1.21",
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^11.0.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",
|
||||
"rollup": "^2.35.1",
|
||||
"rollup-plugin-terser": "^7.0.2"
|
||||
},
|
||||
"author": "Greper",
|
||||
"license": "MIT",
|
||||
"sideEffects": false,
|
||||
"gitHead": "4a421d5b142d453203c68ce6d1036e168ea2455b"
|
||||
}
|
||||
21
packages/core/executor/rollup.config.js
Normal file
21
packages/core/executor/rollup.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import json from '@rollup/plugin-json'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve'
|
||||
|
||||
export default {
|
||||
input: 'src/index.js',
|
||||
output: [
|
||||
{
|
||||
file: 'bundle.js',
|
||||
format: 'es'
|
||||
},
|
||||
{
|
||||
file: 'bundle.min.js',
|
||||
format: 'iife',
|
||||
name: 'version',
|
||||
plugins: [terser()]
|
||||
}
|
||||
],
|
||||
plugins: [json(), commonjs(), nodeResolve()]
|
||||
}
|
||||
182
packages/core/executor/src/index.js
Normal file
182
packages/core/executor/src/index.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import { Certd } from '@certd/certd'
|
||||
import { pluginRegistry, util } from '@certd/api'
|
||||
import _ from 'lodash-es'
|
||||
import dayjs from 'dayjs'
|
||||
import { Trace } from './trace.js'
|
||||
const logger = util.logger
|
||||
|
||||
function createDefaultOptions () {
|
||||
return {
|
||||
args: {
|
||||
forceCert: false,
|
||||
forceDeploy: true,
|
||||
forceRedeploy: false,
|
||||
doNotThrowError: false // 部署流程执行有错误时,不抛异常,此时整个任务执行完毕后,可以返回结果,你可以在返回结果中处理
|
||||
}
|
||||
}
|
||||
}
|
||||
export class Executor {
|
||||
constructor () {
|
||||
this.trace = new Trace()
|
||||
}
|
||||
|
||||
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)
|
||||
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('任务开始')
|
||||
const certd = new Certd(options)
|
||||
const cert = await this.runCertd(certd)
|
||||
if (cert == null) {
|
||||
throw new Error('申请证书失败')
|
||||
}
|
||||
logger.info('证书保存路径:', cert.certDir)
|
||||
|
||||
logger.info('----------------------')
|
||||
if (!cert.isNew) {
|
||||
// 如果没有更新
|
||||
if (options.args.forceRedeploy) {
|
||||
// 强制重新部署,清空保存的状态
|
||||
await certd.certStore.setCurrentFile('context.json', '{}')
|
||||
} else if (!options.args.forceDeploy) {
|
||||
// 且不需要强制deploy
|
||||
logger.info('证书无更新,无需重新部署')
|
||||
logger.info('任务完成')
|
||||
return { cert }
|
||||
}
|
||||
}
|
||||
// 读取上次执行进度
|
||||
let context = {}
|
||||
const contextJson = await certd.certStore.getCurrentFile('context.json')
|
||||
if (contextJson) {
|
||||
context = JSON.parse(contextJson)
|
||||
}
|
||||
|
||||
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 = resultTrace.get({ })
|
||||
if (result) {
|
||||
if (result.status === 'error' && options.args.doNotThrowError === false) {
|
||||
throw new Error(result.remark)
|
||||
}
|
||||
}
|
||||
return {
|
||||
cert,
|
||||
context,
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
async runCertd (certd) {
|
||||
logger.info(`证书任务 ${JSON.stringify(certd.options.cert.domains)} 开始`)
|
||||
const cert = await certd.certApply()
|
||||
logger.info(`证书任务 ${JSON.stringify(certd.options.cert.domains)} 完成`)
|
||||
return cert
|
||||
}
|
||||
|
||||
async runDeploys ({ options, cert, context, trace }) {
|
||||
if (cert == null) {
|
||||
const certd = new Certd(options)
|
||||
cert = await certd.readCurrentCert()
|
||||
}
|
||||
logger.info('部署任务开始')
|
||||
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('')
|
||||
deployTrace.set({ value: { current: 'skip', status: 'disabled', remark: '流程禁用' } })
|
||||
deployTrace.set({ tasks: null })
|
||||
continue
|
||||
}
|
||||
try {
|
||||
for (const task of deploy.tasks) {
|
||||
if (context[deployName] == null) {
|
||||
context[deployName] = {}
|
||||
}
|
||||
const taskContext = context[deployName]
|
||||
// 开始执行任务列表
|
||||
await this.runTask({ options, cert, task, context: taskContext, deploy, trace })
|
||||
}
|
||||
|
||||
deployTrace.set({ value: { status: 'success', remark: '执行成功' } })
|
||||
trace.set({ type: 'result', value: { status: 'success', remark: '执行成功' } })
|
||||
} catch (e) {
|
||||
deployTrace.set({ value: { status: 'error', remark: '执行失败:' + e.message } })
|
||||
trace.set({ type: 'result', value: { status: 'error', remark: deployName + '执行失败:' + e.message } })
|
||||
logger.error('流程执行失败', e)
|
||||
}
|
||||
|
||||
logger.info('')
|
||||
}
|
||||
}
|
||||
|
||||
async runTask ({ options, task, cert, context, deploy, trace }) {
|
||||
const taskType = task.type
|
||||
const Plugin = pluginRegistry.get(taskType)
|
||||
const deployName = deploy.deployName
|
||||
const taskName = task.taskName
|
||||
if (Plugin == null) {
|
||||
throw new Error(`插件:${taskType}还未安装`)
|
||||
}
|
||||
|
||||
let instance = Plugin
|
||||
if (Plugin instanceof Function) {
|
||||
instance = new Plugin({ accessProviders: options.accessProviders })
|
||||
}
|
||||
const taskTrace = trace.getInstance({ type: 'deploy', deployName, taskName })
|
||||
const traceStatus = taskTrace.get({})
|
||||
if (traceStatus && traceStatus.status === 'success' && !options.args.forceRedeploy) {
|
||||
logger.info(`----【${taskName}】已经执行完成,跳过此任务`)
|
||||
taskTrace.set({ value: { current: 'skip', status: 'success', remark: '已执行成功过,本次跳过' } })
|
||||
return
|
||||
}
|
||||
logger.info(`----【${taskName}】开始执行`)
|
||||
try {
|
||||
// 执行任务
|
||||
await instance.execute({ cert, props: task.props, context })
|
||||
taskTrace.set({ value: { current: 'success', status: 'success', remark: '执行成功', time: dayjs().format() } })
|
||||
} catch (e) {
|
||||
taskTrace.set({ value: { current: 'error', status: 'error', remark: e.message, time: dayjs().format() } })
|
||||
throw e
|
||||
}
|
||||
logger.info(`----任务【${taskName}】执行完成`)
|
||||
logger.info('')
|
||||
}
|
||||
}
|
||||
96
packages/core/executor/src/trace.js
Normal file
96
packages/core/executor/src/trace.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { util } from '@certd/api'
|
||||
import _ from 'lodash-es'
|
||||
const logger = util.logger
|
||||
export class Trace {
|
||||
constructor (context) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
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 ({ type, deployName, taskName, prop }) {
|
||||
return _.get(this.context, this.buildTraceKey({ type, deployName, taskName, prop }))
|
||||
}
|
||||
|
||||
buildTraceKey ({ type = 'default', deployName, taskName, prop }) {
|
||||
let key = '__trace__.' + type
|
||||
if (deployName) {
|
||||
key += '.'
|
||||
key += deployName.replace(/\./g, '_')
|
||||
}
|
||||
if (taskName) {
|
||||
key += '.tasks.'
|
||||
key += taskName.replace(/\./g, '_')
|
||||
}
|
||||
if (prop) {
|
||||
key += '.' + prop
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
getStringLength (str) {
|
||||
const enLength = str.replace(/[\u0391-\uFFE5]/g, '').length // 先把中文替换成两个字节的英文,再计算长度
|
||||
return Math.floor((str.length - enLength) * 1.5) + enLength
|
||||
}
|
||||
|
||||
print () {
|
||||
const context = this.context
|
||||
logger.info('---------------------------任务结果总览--------------------------')
|
||||
if (context.certIsNew) {
|
||||
this.printTraceLine({ current: 'success', remark: '证书更新成功' }, '更新证书')
|
||||
} else {
|
||||
this.printTraceLine({ current: 'skip', remark: '还未到过期时间,跳过' }, '更新证书')
|
||||
}
|
||||
const trace = this.get({ type: 'deploy' })
|
||||
// logger.info('trace', trace)
|
||||
for (const deployName in trace) {
|
||||
if (trace[deployName] == null) {
|
||||
trace[deployName] = {}
|
||||
}
|
||||
const traceStatus = this.printTraceLine(trace[deployName], deployName)
|
||||
|
||||
const tasks = traceStatus.tasks
|
||||
if (tasks) {
|
||||
for (const taskName in tasks) {
|
||||
if (tasks[taskName] == null) {
|
||||
tasks[taskName] = {}
|
||||
}
|
||||
this.printTraceLine(tasks[taskName], taskName, ' └')
|
||||
}
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
printTraceLine (traceStatus, name, prefix = '') {
|
||||
const length = this.getStringLength(name)
|
||||
const endPad = _.repeat('-', 45 - prefix.length - length) + '\t'
|
||||
const status = traceStatus.current || traceStatus.status || ''
|
||||
const remark = traceStatus.remark || ''
|
||||
logger.info(`${prefix}【${name}】${endPad}[${status}] \t${remark}`)
|
||||
return traceStatus
|
||||
}
|
||||
}
|
||||
42
packages/core/executor/test/index.test.js
Normal file
42
packages/core/executor/test/index.test.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import pkg from 'chai'
|
||||
import { Executor } from '../src/index.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(120000)
|
||||
const options = createOptions()
|
||||
const executor = new Executor()
|
||||
const ret = await executor.run(options)
|
||||
expect(ret).ok
|
||||
expect(ret.cert).ok
|
||||
})
|
||||
it('#forceCert', async function () {
|
||||
this.timeout(120000)
|
||||
const executor = new Executor()
|
||||
const options = createOptions()
|
||||
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(120000)
|
||||
const executor = new Executor()
|
||||
const options = createOptions()
|
||||
const ret = await executor.run(options, { forceCert: false, forceDeploy: true, forceRedeploy: true })
|
||||
expect(ret).ok
|
||||
expect(ret.cert).ok
|
||||
})
|
||||
})
|
||||
23
packages/core/executor/webpack.config.cjs
Normal file
23
packages/core/executor/webpack.config.cjs
Normal file
@@ -0,0 +1,23 @@
|
||||
const path = require('path')
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
|
||||
console.log(CleanWebpackPlugin)
|
||||
|
||||
module.exports = {
|
||||
devtool: 'source-map',
|
||||
target: 'node',
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
filename: 'executor.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
library: 'certdExecutor',
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin()
|
||||
],
|
||||
mode: 'production'
|
||||
// mode: 'development',
|
||||
// optimization: {
|
||||
// usedExports: true
|
||||
// }
|
||||
}
|
||||
1936
packages/core/executor/yarn.lock
Normal file
1936
packages/core/executor/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/deploy/client
Submodule
1
packages/deploy/client
Submodule
Submodule packages/deploy/client added at 8eb90445fa
1
packages/deploy/server
Submodule
1
packages/deploy/server
Submodule
Submodule packages/deploy/server added at 07443ac885
1
packages/issuer/node-acme-client
Submodule
1
packages/issuer/node-acme-client
Submodule
Submodule packages/issuer/node-acme-client added at 7003db0125
14
packages/plugins/plugin-aliyun/.eslintrc
Normal file
14
packages/plugins/plugin-aliyun/.eslintrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.test.js", "*.spec.js"],
|
||||
"rules": {
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
3104
packages/plugins/plugin-aliyun/package-lock.json
generated
Normal file
3104
packages/plugins/plugin-aliyun/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
packages/plugins/plugin-aliyun/package.json
Normal file
26
packages/plugins/plugin-aliyun/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@certd/plugin-aliyun",
|
||||
"version": "0.1.19",
|
||||
"description": "",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@alicloud/pop-core": "^1.7.10",
|
||||
"@certd/api": "^0.1.17",
|
||||
"dayjs": "^1.9.7",
|
||||
"lodash-es": "^4.17.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/certd": "^0.1.19",
|
||||
"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": "4a421d5b142d453203c68ce6d1036e168ea2455b"
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
131
packages/plugins/plugin-aliyun/src/dns-providers/aliyun.js
Normal file
131
packages/plugins/plugin-aliyun/src/dns-providers/aliyun.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import { AbstractDnsProvider } from '@certd/api'
|
||||
import Core from '@alicloud/pop-core'
|
||||
import _ from 'lodash-es'
|
||||
export class AliyunDnsProvider extends AbstractDnsProvider {
|
||||
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: accessProvider.accessKeyId,
|
||||
accessKeySecret: accessProvider.accessKeySecret,
|
||||
endpoint: 'https://alidns.aliyuncs.com',
|
||||
apiVersion: '2015-01-09'
|
||||
})
|
||||
}
|
||||
|
||||
async getDomainList () {
|
||||
const params = {
|
||||
RegionId: 'cn-hangzhou'
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
const ret = await this.client.request('DescribeDomains', params, requestOption)
|
||||
return ret.Domains.Domain
|
||||
}
|
||||
|
||||
async matchDomain (dnsRecord) {
|
||||
const list = await this.getDomainList()
|
||||
let domain = null
|
||||
for (const item of list) {
|
||||
if (_.endsWith(dnsRecord, item.DomainName)) {
|
||||
domain = item.DomainName
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!domain) {
|
||||
throw new Error('can not find Domain ,' + dnsRecord)
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
async getRecords (domain, rr, value) {
|
||||
const params = {
|
||||
RegionId: 'cn-hangzhou',
|
||||
DomainName: domain,
|
||||
RRKeyWord: rr
|
||||
}
|
||||
if (value) {
|
||||
params.ValueKeyWord = value
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
const ret = await this.client.request('DescribeDomainRecords', params, requestOption)
|
||||
return ret.DomainRecords.Record
|
||||
}
|
||||
|
||||
async createRecord ({ fullRecord, type, value }) {
|
||||
this.logger.info('添加域名解析:', fullRecord, value)
|
||||
const domain = await this.matchDomain(fullRecord)
|
||||
const rr = fullRecord.replace('.' + domain, '')
|
||||
|
||||
const params = {
|
||||
RegionId: 'cn-hangzhou',
|
||||
DomainName: domain,
|
||||
RR: rr,
|
||||
Type: type,
|
||||
Value: value
|
||||
// Line: 'oversea' // 海外
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
try {
|
||||
const ret = await this.client.request('AddDomainRecord', params, requestOption)
|
||||
this.logger.info('添加域名解析成功:', value, value, ret.RecordId)
|
||||
return ret.RecordId
|
||||
} catch (e) {
|
||||
if (e.code === 'DomainRecordDuplicate') {
|
||||
return
|
||||
}
|
||||
this.logger.info('添加域名解析出错', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async removeRecord ({ fullRecord, type, value, record }) {
|
||||
const params = {
|
||||
RegionId: 'cn-hangzhou',
|
||||
RecordId: record
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
const ret = await this.client.request('DeleteDomainRecord', params, requestOption)
|
||||
this.logger.info('删除域名解析成功:', fullRecord, value, ret.RecordId)
|
||||
return ret.RecordId
|
||||
}
|
||||
}
|
||||
24
packages/plugins/plugin-aliyun/src/index.js
Normal file
24
packages/plugins/plugin-aliyun/src/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
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 { pluginRegistry, accessProviderRegistry, dnsProviderRegistry } from '@certd/api'
|
||||
|
||||
export const Plugins = {
|
||||
UploadCertToAliyun,
|
||||
DeployCertToAliyunCDN
|
||||
}
|
||||
export default {
|
||||
install () {
|
||||
_.forEach(Plugins, item => {
|
||||
pluginRegistry.install(item)
|
||||
})
|
||||
|
||||
accessProviderRegistry.install(AliyunAccessProvider)
|
||||
dnsProviderRegistry.install(AliyunDnsProvider)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { AbstractPlugin } from '@certd/api'
|
||||
|
||||
export class AbstractAliyunPlugin extends AbstractPlugin {
|
||||
checkRet (ret) {
|
||||
if (ret.code != null) {
|
||||
throw new Error('执行失败:', ret.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import Core from '@alicloud/pop-core'
|
||||
import { AbstractAliyunPlugin } from '../abstract-aliyun.js'
|
||||
|
||||
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'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
output: {
|
||||
aliyunCertId: {
|
||||
type: String,
|
||||
desc: '上传成功后的阿里云CertId'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class UploadCertToAliyun extends AbstractAliyunPlugin {
|
||||
static define () {
|
||||
return define
|
||||
}
|
||||
|
||||
getClient (aliyunProvider) {
|
||||
return new Core({
|
||||
accessKeyId: aliyunProvider.accessKeyId,
|
||||
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||
endpoint: 'https://cas.aliyuncs.com',
|
||||
apiVersion: '2018-07-13'
|
||||
})
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const { name, accessProvider } = props
|
||||
const certName = this.appendTimeSuffix(name || cert.domain)
|
||||
const params = {
|
||||
RegionId: props.regionId || 'cn-hangzhou',
|
||||
Name: certName,
|
||||
Cert: cert.crt,
|
||||
Key: cert.key
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
const provider = this.getAccessProvider(accessProvider)
|
||||
const client = this.getClient(provider)
|
||||
const ret = await client.request('CreateUserCertificate', params, requestOption)
|
||||
this.checkRet(ret)
|
||||
this.logger.info('证书上传成功:aliyunCertId=', ret.CertId)
|
||||
context.aliyunCertId = ret.CertId
|
||||
}
|
||||
|
||||
/**
|
||||
* 没用,现在阿里云证书不允许删除
|
||||
* @param accessProviders
|
||||
* @param cert
|
||||
* @param props
|
||||
* @param context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async rollback ({ cert, props, context }) {
|
||||
const { accessProvider } = props
|
||||
const { aliyunCertId } = context
|
||||
this.logger.info('准备删除阿里云证书:', aliyunCertId)
|
||||
const params = {
|
||||
RegionId: props.regionId || 'cn-hangzhou',
|
||||
CertId: aliyunCertId
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
const provider = this.getAccessProvider(accessProvider)
|
||||
const client = this.getClient(provider)
|
||||
const ret = await client.request('DeleteUserCertificate', params, requestOption)
|
||||
this.checkRet(ret)
|
||||
this.logger.info('证书删除成功:', aliyunCertId)
|
||||
delete context.aliyunCertId
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import pkg from 'chai'
|
||||
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 () {
|
||||
this.timeout(300000)
|
||||
const options = createOptions()
|
||||
options.args = { forceCert: true, test: false }
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.certApply()
|
||||
expect(cert).ok
|
||||
expect(cert.crt).ok
|
||||
expect(cert.key).ok
|
||||
expect(cert.detail).ok
|
||||
expect(cert.expires).ok
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import pkg from 'chai'
|
||||
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 = 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 = 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 = 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
|
||||
|
||||
const recordId = await aliyunDnsProvider.removeRecord({ fullRecord: '___certd___.__test__.docmirror.cn', type: 'TXT', value: 'aaaa', record })
|
||||
console.log('recordId', recordId)
|
||||
expect(recordId != null).ok
|
||||
})
|
||||
})
|
||||
42
packages/plugins/plugin-aliyun/test/options.js
Normal file
42
packages/plugins/plugin-aliyun/test/options.js
Normal 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
|
||||
@@ -0,0 +1,21 @@
|
||||
import pkg from 'chai'
|
||||
import { DeployCertToAliyunCDN } from '../../src/plugins/deploy-to-cdn/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
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(options)
|
||||
options.cert.domains = ['*.docmirror.cn', 'docmirror.cn']
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const ret = await plugin.doExecute({
|
||||
cert,
|
||||
props: { domainName: 'certd-cdn-upload.docmirror.cn', certName: 'certd部署测试', from: 'cas', accessProvider: 'aliyun' }
|
||||
})
|
||||
console.log('context:', context, ret)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
import pkg from 'chai'
|
||||
import { UploadCertToAliyun } from '../../src/plugins/upload-to-aliyun/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
const { expect } = pkg
|
||||
describe('PluginUploadToAliyun', function () {
|
||||
it('#execute', async function () {
|
||||
this.timeout(5000)
|
||||
const options = createOptions()
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['_.docmirror.cn']
|
||||
const plugin = new UploadCertToAliyun(options)
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const context = {}
|
||||
const deployOpts = {
|
||||
cert,
|
||||
props: { accessProvider: 'aliyun' },
|
||||
context
|
||||
}
|
||||
await plugin.doExecute(deployOpts)
|
||||
console.log('context:', context)
|
||||
|
||||
// await plugin.sleep(1000)
|
||||
// await plugin.rollback(deployOpts)
|
||||
})
|
||||
})
|
||||
1858
packages/plugins/plugin-aliyun/yarn-error.log
Normal file
1858
packages/plugins/plugin-aliyun/yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
2922
packages/plugins/plugin-aliyun/yarn.lock
Normal file
2922
packages/plugins/plugin-aliyun/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
14
packages/plugins/plugin-host/.eslintrc
Normal file
14
packages/plugins/plugin-host/.eslintrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.test.js", "*.spec.js"],
|
||||
"rules": {
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
3166
packages/plugins/plugin-host/package-lock.json
generated
Normal file
3166
packages/plugins/plugin-host/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
packages/plugins/plugin-host/package.json
Normal file
26
packages/plugins/plugin-host/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@certd/plugin-host",
|
||||
"version": "0.1.21",
|
||||
"description": "",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@certd/api": "^0.1.17",
|
||||
"dayjs": "^1.9.7",
|
||||
"lodash-es": "^4.17.20",
|
||||
"ssh2": "^0.8.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/certd": "^0.1.19",
|
||||
"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": "4a421d5b142d453203c68ce6d1036e168ea2455b"
|
||||
}
|
||||
26
packages/plugins/plugin-host/src/access-providers/ssh.js
Normal file
26
packages/plugins/plugin-host/src/access-providers/ssh.js
Normal 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: '密钥,密码或此项必填一项'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
packages/plugins/plugin-host/src/index.js
Normal file
22
packages/plugins/plugin-host/src/index.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { AbstractPlugin } from '@certd/api'
|
||||
|
||||
export class AbstractHostPlugin extends AbstractPlugin {
|
||||
checkRet (ret) {
|
||||
if (ret.code != null) {
|
||||
throw new Error('执行失败:', ret.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { AbstractHostPlugin } from '../abstract-host.js'
|
||||
import { SshClient } from '../ssh.js'
|
||||
export class HostShellExecute extends AbstractHostPlugin {
|
||||
/**
|
||||
* 插件定义
|
||||
* 名称
|
||||
* 入参
|
||||
* 出参
|
||||
*/
|
||||
static define () {
|
||||
return {
|
||||
name: 'hostShellExecute',
|
||||
label: '执行远程主机脚本命令',
|
||||
input: {
|
||||
accessProvider: {
|
||||
label: '主机登录配置',
|
||||
type: [String, Object],
|
||||
desc: '登录',
|
||||
component: {
|
||||
name: 'access-provider-selector',
|
||||
filter: 'ssh'
|
||||
},
|
||||
required: true
|
||||
},
|
||||
script: {
|
||||
label: 'shell脚本命令',
|
||||
component: {
|
||||
name: 'a-textarea'
|
||||
}
|
||||
}
|
||||
},
|
||||
output: {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const { script, accessProvider } = props
|
||||
const connectConf = this.getAccessProvider(accessProvider)
|
||||
const sshClient = new SshClient()
|
||||
const ret = await sshClient.exec({
|
||||
connectConf,
|
||||
script
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cert
|
||||
* @param props
|
||||
* @param context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async rollback ({ cert, props, context }) {
|
||||
|
||||
}
|
||||
}
|
||||
130
packages/plugins/plugin-host/src/plugins/ssh.js
Normal file
130
packages/plugins/plugin-host/src/plugins/ssh.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import ssh2 from 'ssh2'
|
||||
import path from 'path'
|
||||
import { util } from '@certd/api'
|
||||
import _ from 'lodash-es'
|
||||
const logger = util.logger
|
||||
export class SshClient {
|
||||
/**
|
||||
*
|
||||
* @param connectConf
|
||||
{
|
||||
host: '192.168.100.100',
|
||||
port: 22,
|
||||
username: 'frylock',
|
||||
password: 'nodejsrules'
|
||||
}
|
||||
* @param transports
|
||||
*/
|
||||
uploadFiles ({ connectConf, transports, sudo = false }) {
|
||||
const conn = new ssh2.Client()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
conn.on('ready', () => {
|
||||
logger.info('连接服务器成功')
|
||||
conn.sftp(async (err, sftp) => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
|
||||
try {
|
||||
for (const transport of transports) {
|
||||
logger.info('上传文件:', JSON.stringify(transport))
|
||||
sudo = sudo ? 'sudo' : ''
|
||||
await this.exec({ connectConf, script: `${sudo} mkdir -p ${path.dirname(transport.remotePath)} ` })
|
||||
await this.fastPut({ sftp, ...transport })
|
||||
}
|
||||
resolve()
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
} finally {
|
||||
conn.end()
|
||||
}
|
||||
})
|
||||
}).connect(connectConf)
|
||||
})
|
||||
}
|
||||
|
||||
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({
|
||||
connectConf,
|
||||
onReady: (conn) => {
|
||||
conn.shell((err, stream) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
const output = []
|
||||
stream.on('close', () => {
|
||||
logger.info('Stream :: close')
|
||||
conn.end()
|
||||
resolve(output)
|
||||
}).on('data', (data) => {
|
||||
logger.info('' + data)
|
||||
output.push('' + data)
|
||||
})
|
||||
stream.end(script + '\nexit\n')
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
connect ({ connectConf, onReady }) {
|
||||
const conn = new ssh2.Client()
|
||||
conn.on('ready', () => {
|
||||
console.log('Client :: ready')
|
||||
onReady(conn)
|
||||
}).connect(connectConf)
|
||||
return conn
|
||||
}
|
||||
|
||||
fastPut ({ sftp, localPath, remotePath }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
sftp.fastPut(localPath, remotePath, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { AbstractHostPlugin } from '../abstract-host.js'
|
||||
import { SshClient } from '../ssh.js'
|
||||
export class UploadCertToHost extends AbstractHostPlugin {
|
||||
/**
|
||||
* 插件定义
|
||||
* 名称
|
||||
* 入参
|
||||
* 出参
|
||||
*/
|
||||
static define () {
|
||||
return {
|
||||
name: 'uploadCertToHost',
|
||||
label: '上传证书到主机',
|
||||
input: {
|
||||
crtPath: {
|
||||
label: '证书保存路径'
|
||||
},
|
||||
keyPath: {
|
||||
label: '私钥保存路径'
|
||||
},
|
||||
accessProvider: {
|
||||
label: '主机登录配置',
|
||||
type: [String, Object],
|
||||
desc: 'access授权',
|
||||
component: {
|
||||
name: 'access-provider-selector',
|
||||
filter: 'ssh'
|
||||
},
|
||||
required: true
|
||||
},
|
||||
sudo: {
|
||||
label: '是否sudo'
|
||||
}
|
||||
},
|
||||
output: {
|
||||
hostCrtPath: {
|
||||
type: String,
|
||||
desc: '上传成功后的证书路径'
|
||||
},
|
||||
hostKeyPath: {
|
||||
type: String,
|
||||
desc: '上传成功后的私钥路径'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const { crtPath, keyPath, accessProvider } = props
|
||||
const connectConf = this.getAccessProvider(accessProvider)
|
||||
const sshClient = new SshClient()
|
||||
await sshClient.uploadFiles({
|
||||
connectConf,
|
||||
transports: [
|
||||
{
|
||||
localPath: cert.crtPath,
|
||||
remotePath: crtPath
|
||||
},
|
||||
{
|
||||
localPath: cert.keyPath,
|
||||
remotePath: keyPath
|
||||
}
|
||||
]
|
||||
})
|
||||
this.logger.info('证书上传成功:crtPath=', crtPath, ',keyPath=', keyPath)
|
||||
|
||||
context.hostCrtPath = crtPath
|
||||
context.hostKeyPath = keyPath
|
||||
return {
|
||||
hostCrtPath: crtPath,
|
||||
hostKeyPath: keyPath
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cert
|
||||
* @param props
|
||||
* @param context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async rollback ({ cert, props, context }) {
|
||||
|
||||
}
|
||||
}
|
||||
42
packages/plugins/plugin-host/test/options.js
Normal file
42
packages/plugins/plugin-host/test/options.js
Normal 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: '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
|
||||
@@ -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))
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
1858
packages/plugins/plugin-host/yarn-error.log
Normal file
1858
packages/plugins/plugin-host/yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
2922
packages/plugins/plugin-host/yarn.lock
Normal file
2922
packages/plugins/plugin-host/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
14
packages/plugins/plugin-tencent/.eslintrc
Normal file
14
packages/plugins/plugin-tencent/.eslintrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.test.js", "*.spec.js"],
|
||||
"rules": {
|
||||
"no-unused-expressions": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
3752
packages/plugins/plugin-tencent/package-lock.json
generated
Normal file
3752
packages/plugins/plugin-tencent/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
packages/plugins/plugin-tencent/package.json
Normal file
27
packages/plugins/plugin-tencent/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@certd/plugin-tencent",
|
||||
"version": "0.1.21",
|
||||
"description": "",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@certd/api": "^0.1.17",
|
||||
"dayjs": "^1.9.7",
|
||||
"kubernetes-client": "^9.0.0",
|
||||
"lodash-es": "^4.17.20",
|
||||
"tencentcloud-sdk-nodejs": "^4.0.44"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/certd": "^0.1.19",
|
||||
"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": "4a421d5b142d453203c68ce6d1036e168ea2455b"
|
||||
}
|
||||
@@ -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: '该项必填' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: '该项必填' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
packages/plugins/plugin-tencent/src/dns-providers/dnspod.js
Normal file
97
packages/plugins/plugin-tencent/src/dns-providers/dnspod.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { AbstractDnsProvider, util } from '@certd/api'
|
||||
import _ from 'lodash-es'
|
||||
const request = util.request
|
||||
export class DnspodDnsProvider extends AbstractDnsProvider {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: {
|
||||
login_token: this.loginToken,
|
||||
format: 'json',
|
||||
lang: 'cn',
|
||||
error_on_empty: 'no'
|
||||
},
|
||||
timeout: 5000
|
||||
}
|
||||
_.merge(config, options)
|
||||
|
||||
const ret = await request(config)
|
||||
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
|
||||
}
|
||||
|
||||
async getDomainList () {
|
||||
const ret = await this.doRequest({
|
||||
url: 'https://dnsapi.cn/Domain.List'
|
||||
})
|
||||
this.logger.debug('dnspod 域名列表:', ret.domains)
|
||||
return ret.domains
|
||||
}
|
||||
|
||||
async createRecord ({ fullRecord, type, value }) {
|
||||
this.logger.info('添加域名解析:', fullRecord, value)
|
||||
const domainItem = await this.matchDomain(fullRecord, 'name')
|
||||
const domain = domainItem.name
|
||||
const rr = fullRecord.replace('.' + domain, '')
|
||||
|
||||
const ret = await this.doRequest({
|
||||
url: 'https://dnsapi.cn/Record.Create',
|
||||
formData: {
|
||||
domain,
|
||||
sub_domain: rr,
|
||||
record_type: type,
|
||||
record_line: '默认',
|
||||
value: value,
|
||||
mx: 1
|
||||
}
|
||||
}, ['104'])// 104错误码为记录已存在,无需再次添加
|
||||
this.logger.info('添加域名解析成功:', fullRecord, value, JSON.stringify(ret.record))
|
||||
return ret.record
|
||||
}
|
||||
|
||||
async removeRecord ({ fullRecord, type, value, record }) {
|
||||
const domain = await this.matchDomain(fullRecord, 'name')
|
||||
|
||||
const ret = await this.doRequest({
|
||||
url: 'https://dnsapi.cn/Record.Remove',
|
||||
formData: {
|
||||
domain,
|
||||
record_id: record.id
|
||||
}
|
||||
})
|
||||
this.logger.info('删除域名解析成功:', fullRecord, value)
|
||||
return ret.RecordId
|
||||
}
|
||||
}
|
||||
34
packages/plugins/plugin-tencent/src/index.js
Normal file
34
packages/plugins/plugin-tencent/src/index.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { AbstractPlugin } from '@certd/api'
|
||||
|
||||
export class AbstractTencentPlugin extends AbstractPlugin {
|
||||
checkRet (ret) {
|
||||
if (!ret || ret.Error) {
|
||||
throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message)
|
||||
}
|
||||
}
|
||||
|
||||
getSafetyDomain (domain) {
|
||||
return domain.replace(/\*/g, '_')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { AbstractTencentPlugin } from '../abstract-tencent.js'
|
||||
import dayjs from 'dayjs'
|
||||
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
||||
|
||||
export class DeployCertToTencentCDN extends AbstractTencentPlugin {
|
||||
/**
|
||||
* 插件定义
|
||||
* 名称
|
||||
* 入参
|
||||
* 出参
|
||||
*/
|
||||
static define () {
|
||||
return {
|
||||
name: 'deployCertToTencentCDN',
|
||||
label: '部署到腾讯云CDN',
|
||||
input: {
|
||||
domainName: {
|
||||
label: 'cdn加速域名',
|
||||
required: true
|
||||
},
|
||||
certName: {
|
||||
label: '证书名称',
|
||||
desc: '证书上传后将以此参数作为名称前缀'
|
||||
},
|
||||
certType: {
|
||||
default: 'upload',
|
||||
label: '证书来源',
|
||||
options: [
|
||||
{ value: 'upload', label: '直接上传' },
|
||||
{ value: 'cloud', label: '从证书库', desc: '需要uploadCertToTencent作为前置任务' }
|
||||
],
|
||||
desc: '如果选择‘从证书库’类型,则需要以《上传证书到腾讯云》作为前置任务',
|
||||
required: true
|
||||
},
|
||||
accessProvider: {
|
||||
label: 'Access提供者',
|
||||
type: [String, Object],
|
||||
desc: 'access 授权',
|
||||
component: {
|
||||
name: 'access-provider-selector',
|
||||
filter: 'tencent'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
output: {
|
||||
tencentCertId: {
|
||||
type: String,
|
||||
desc: '证书来源选择上传时,将返回此id'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
async rollback ({ cert, props, context }) {
|
||||
|
||||
}
|
||||
|
||||
getClient (accessProvider) {
|
||||
const CdnClient = tencentcloud.cdn.v20180606.Client
|
||||
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
secretKey: accessProvider.secretKey
|
||||
},
|
||||
region: '',
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: 'cdn.tencentcloudapi.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new CdnClient(clientConfig)
|
||||
}
|
||||
|
||||
buildParams (props, context, cert) {
|
||||
const { domainName, from } = props
|
||||
const { tencentCertId } = context
|
||||
this.logger.info('部署腾讯云证书ID:', tencentCertId)
|
||||
const params = {
|
||||
Https: {
|
||||
Switch: 'on',
|
||||
CertInfo: {
|
||||
CertId: tencentCertId
|
||||
// Certificate: '1231',
|
||||
// PrivateKey: '1231'
|
||||
}
|
||||
},
|
||||
Domain: domainName
|
||||
}
|
||||
if (from === 'upload' || tencentCertId == null) {
|
||||
params.Https.CertInfo = {
|
||||
Certificate: cert.crt,
|
||||
PrivateKey: cert.key
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
async doRequest (client, params) {
|
||||
const ret = await client.UpdateDomainConfig(params)
|
||||
this.checkRet(ret)
|
||||
this.logger.info('设置腾讯云CDN证书成功:', ret.RequestId)
|
||||
return ret.RequestId
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { AbstractTencentPlugin } from '../abstract-tencent.js'
|
||||
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
||||
export class DeployCertToTencentCLB extends AbstractTencentPlugin {
|
||||
/**
|
||||
* 插件定义
|
||||
* 名称
|
||||
* 入参
|
||||
* 出参
|
||||
*/
|
||||
static define () {
|
||||
return {
|
||||
name: 'deployCertToTencentCLB',
|
||||
label: '部署到腾讯云CLB',
|
||||
desc: '暂时只支持单向认证证书,暂时只支持通用负载均衡',
|
||||
input: {
|
||||
region: {
|
||||
label: '大区',
|
||||
default: 'ap-guangzhou',
|
||||
required: true
|
||||
},
|
||||
domain: {
|
||||
label: '域名',
|
||||
type: [String, Array],
|
||||
required: true,
|
||||
desc: '要更新的支持https的负载均衡的域名'
|
||||
},
|
||||
loadBalancerId: {
|
||||
label: '负载均衡ID',
|
||||
desc: '如果没有配置,则根据域名匹配负载均衡下的监听器(根据域名匹配时暂时只支持前100个)',
|
||||
required: true
|
||||
},
|
||||
listenerId: {
|
||||
label: '监听器ID',
|
||||
desc: '如果没有配置,则根据域名或负载均衡id匹配监听器'
|
||||
},
|
||||
certName: {
|
||||
label: '证书名称',
|
||||
desc: '如无uploadCertToTencent作为前置,则此项需要设置,默认为域名'
|
||||
},
|
||||
accessProvider: {
|
||||
label: 'Access提供者',
|
||||
type: [String, Object],
|
||||
desc: 'access授权',
|
||||
component: {
|
||||
name: 'access-provider-selector',
|
||||
filter: 'tencent'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
output: {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||
const { region } = props
|
||||
const client = this.getClient(accessProvider, region)
|
||||
|
||||
const lastCertId = await this.getCertIdFromProps(client, props)
|
||||
if (!props.domain) {
|
||||
await this.updateListener(client, cert, props, context)
|
||||
} else {
|
||||
await this.updateByDomainAttr(client, cert, props, context)
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sleep(2000)
|
||||
let newCertId = await this.getCertIdFromProps(client, props)
|
||||
if ((lastCertId && newCertId === lastCertId) || (!lastCertId && !newCertId)) {
|
||||
await this.sleep(2000)
|
||||
newCertId = await this.getCertIdFromProps(client, props)
|
||||
}
|
||||
if (newCertId === lastCertId) {
|
||||
return {}
|
||||
}
|
||||
this.logger.info('腾讯云证书ID:', newCertId)
|
||||
if (!context.tencentCertId) {
|
||||
context.tencentCertId = newCertId
|
||||
}
|
||||
return { tencentCertId: newCertId }
|
||||
} catch (e) {
|
||||
this.logger.warn('查询腾讯云证书失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async getCertIdFromProps (client, props) {
|
||||
const listenerRet = await this.getListenerList(client, props.loadBalancerId, [props.listenerId])
|
||||
return this.getCertIdFromListener(listenerRet[0], props.domain)
|
||||
}
|
||||
|
||||
getCertIdFromListener (listener, domain) {
|
||||
let certId
|
||||
if (!domain) {
|
||||
certId = listener.Certificate.CertId
|
||||
} else {
|
||||
if (listener.Rules && listener.Rules.length > 0) {
|
||||
for (const rule of listener.Rules) {
|
||||
if (rule.Domain === domain) {
|
||||
if (rule.Certificate != null) {
|
||||
certId = rule.Certificate.CertId
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return certId
|
||||
}
|
||||
|
||||
async rollback ({ cert, props, context }) {
|
||||
this.logger.warn('未实现rollback')
|
||||
}
|
||||
|
||||
async updateListener (client, cert, props, context) {
|
||||
const params = this.buildProps(props, context, cert)
|
||||
const ret = await client.ModifyListener(params)
|
||||
this.checkRet(ret)
|
||||
this.logger.info('设置腾讯云CLB证书成功:', ret.RequestId, '->loadBalancerId:', props.loadBalancerId, 'listenerId', props.listenerId)
|
||||
return ret
|
||||
}
|
||||
|
||||
async updateByDomainAttr (client, cert, props, context) {
|
||||
const params = this.buildProps(props, context, cert)
|
||||
params.Domain = props.domain
|
||||
const ret = await client.ModifyDomainAttributes(params)
|
||||
this.checkRet(ret)
|
||||
this.logger.info('设置腾讯云CLB证书(sni)成功:', ret.RequestId, '->loadBalancerId:', props.loadBalancerId, 'listenerId', props.listenerId, 'domain:', props.domain)
|
||||
return ret
|
||||
}
|
||||
|
||||
buildProps (props, context, cert) {
|
||||
const { certName } = props
|
||||
const { tencentCertId } = context
|
||||
this.logger.info('部署腾讯云证书ID:', tencentCertId)
|
||||
const params = {
|
||||
Certificate: {
|
||||
SSLMode: 'UNIDIRECTIONAL', // 单向认证
|
||||
CertId: tencentCertId
|
||||
},
|
||||
LoadBalancerId: props.loadBalancerId,
|
||||
ListenerId: props.listenerId
|
||||
}
|
||||
|
||||
if (tencentCertId == null) {
|
||||
params.Certificate.CertName = this.appendTimeSuffix(certName || cert.domain)
|
||||
params.Certificate.CertKey = cert.key
|
||||
params.Certificate.CertContent = cert.crt
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
async getCLBList (client, props) {
|
||||
const params = {
|
||||
Limit: 100, // 最大暂时只支持100个,暂时没做翻页
|
||||
OrderBy: 'CreateTime',
|
||||
OrderType: 0,
|
||||
...props.DescribeLoadBalancers
|
||||
}
|
||||
const ret = await client.DescribeLoadBalancers(params)
|
||||
this.checkRet(ret)
|
||||
return ret.LoadBalancerSet
|
||||
}
|
||||
|
||||
async getListenerList (client, balancerId, listenerIds) {
|
||||
// HTTPS
|
||||
const params = {
|
||||
LoadBalancerId: balancerId,
|
||||
Protocol: 'HTTPS',
|
||||
ListenerIds: listenerIds
|
||||
}
|
||||
const ret = await client.DescribeListeners(params)
|
||||
this.checkRet(ret)
|
||||
return ret.Listeners
|
||||
}
|
||||
|
||||
getClient (accessProvider, region) {
|
||||
const ClbClient = tencentcloud.clb.v20180317.Client
|
||||
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
secretKey: accessProvider.secretKey
|
||||
},
|
||||
region: region,
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: 'clb.tencentcloudapi.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ClbClient(clientConfig)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { AbstractTencentPlugin } from '../abstract-tencent.js'
|
||||
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
||||
import { K8sClient } from '../../utils/util.k8s.client.js'
|
||||
export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin {
|
||||
/**
|
||||
* 插件定义
|
||||
* 名称
|
||||
* 入参
|
||||
* 出参
|
||||
*/
|
||||
static define () {
|
||||
return {
|
||||
name: 'deployCertToTencentTKEIngress',
|
||||
label: '部署到腾讯云TKE-ingress',
|
||||
desc: '需要【上传到腾讯云】作为前置任务',
|
||||
input: {
|
||||
region: {
|
||||
label: '大区',
|
||||
default: 'ap-guangzhou',
|
||||
required: true
|
||||
},
|
||||
clusterId: {
|
||||
label: '集群ID',
|
||||
required: true,
|
||||
desc: '例如:cls-6lbj1vee',
|
||||
request: true
|
||||
},
|
||||
namespace: {
|
||||
label: '集群namespace',
|
||||
default: 'default',
|
||||
required: true
|
||||
},
|
||||
secreteName: {
|
||||
type: [String, Array],
|
||||
label: '证书的secret名称',
|
||||
desc: '支持多个(传入数组)',
|
||||
required: true
|
||||
},
|
||||
ingressName: {
|
||||
type: [String, Array],
|
||||
label: 'ingress名称',
|
||||
desc: '支持多个(传入数组)'
|
||||
},
|
||||
ingressClass: {
|
||||
type: String,
|
||||
label: 'ingress类型',
|
||||
desc: '可选 qcloud / nginx'
|
||||
},
|
||||
clusterIp: {
|
||||
type: String,
|
||||
label: '集群内网ip',
|
||||
desc: '如果开启了外网的话,无需设置'
|
||||
},
|
||||
clusterDomain: {
|
||||
type: String,
|
||||
label: '集群域名',
|
||||
desc: '可不填,默认为:[clusterId].ccs.tencent-cloud.com'
|
||||
},
|
||||
/**
|
||||
* AccessProvider的key,或者一个包含access的具体的对象
|
||||
*/
|
||||
accessProvider: {
|
||||
label: 'Access授权',
|
||||
type: [String, Object],
|
||||
desc: 'access授权',
|
||||
component: {
|
||||
name: 'access-provider-selector',
|
||||
filter: 'tencent'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
output: {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context }) {
|
||||
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||
const tkeClient = this.getTkeClient(accessProvider, props.region)
|
||||
const kubeConfigStr = await this.getTkeKubeConfig(tkeClient, props.clusterId)
|
||||
|
||||
this.logger.info('kubeconfig已成功获取')
|
||||
const k8sClient = new K8sClient(kubeConfigStr)
|
||||
if (props.clusterIp != null) {
|
||||
let clusterDomain = props.clusterDomain
|
||||
if (!clusterDomain) {
|
||||
clusterDomain = `${props.clusterId}.ccs.tencent-cloud.com`
|
||||
}
|
||||
// 修改内网解析ip地址
|
||||
k8sClient.setLookup({ [clusterDomain]: { ip: props.clusterIp } })
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
getTkeClient (accessProvider, region = 'ap-guangzhou') {
|
||||
const TkeClient = tencentcloud.tke.v20180525.Client
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
secretKey: accessProvider.secretKey
|
||||
},
|
||||
region,
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: 'tke.tencentcloudapi.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new TkeClient(clientConfig)
|
||||
}
|
||||
|
||||
async getTkeKubeConfig (client, clusterId) {
|
||||
// Depends on tencentcloud-sdk-nodejs version 4.0.3 or higher
|
||||
const params = {
|
||||
ClusterId: clusterId
|
||||
}
|
||||
const ret = await client.DescribeClusterKubeconfig(params)
|
||||
this.checkRet(ret)
|
||||
this.logger.info('注意:后续操作需要在【集群->基本信息】中开启外网或内网访问,https://console.cloud.tencent.com/tke2/cluster')
|
||||
return ret.Kubeconfig
|
||||
}
|
||||
|
||||
async patchQcloudCertSecret ({ k8sClient, props, context }) {
|
||||
const { tencentCertId } = context
|
||||
if (tencentCertId == null) {
|
||||
throw new Error('请先将【上传证书到腾讯云】作为前置任务')
|
||||
}
|
||||
this.logger.info('腾讯云证书ID:', tencentCertId)
|
||||
const certIdBase64 = Buffer.from(tencentCertId).toString('base64')
|
||||
|
||||
const { namespace, secretName } = props
|
||||
|
||||
const body = {
|
||||
data: {
|
||||
qcloud_cert_id: certIdBase64
|
||||
},
|
||||
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 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
|
||||
|
||||
const body = {
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix('certd')
|
||||
}
|
||||
}
|
||||
}
|
||||
let ingressNames = ingressName
|
||||
if (typeof ingressName === 'string') {
|
||||
ingressNames = [ingressName]
|
||||
}
|
||||
for (const ingress of ingressNames) {
|
||||
await k8sClient.patchIngress({ namespace, ingressName: ingress, body })
|
||||
this.logger.info(`ingress已重启:${ingress}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
||||
import { AbstractTencentPlugin } from '../abstract-tencent.js'
|
||||
|
||||
export class UploadCertToTencent extends AbstractTencentPlugin {
|
||||
/**
|
||||
* 插件定义
|
||||
* 名称
|
||||
* 入参
|
||||
* 出参
|
||||
*/
|
||||
static define () {
|
||||
return {
|
||||
name: 'uploadCertToTencent',
|
||||
label: '上传证书到腾讯云',
|
||||
input: {
|
||||
name: {
|
||||
label: '证书名称'
|
||||
},
|
||||
accessProvider: {
|
||||
label: 'Access授权',
|
||||
type: [String, Object],
|
||||
desc: 'access授权',
|
||||
component: {
|
||||
name: 'access-provider-selector',
|
||||
filter: 'tencent'
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
output: {
|
||||
tencentCertId: {
|
||||
type: String,
|
||||
desc: '上传成功后的腾讯云CertId'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getClient (accessProvider) {
|
||||
const SslClient = tencentcloud.ssl.v20191205.Client
|
||||
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: accessProvider.secretId,
|
||||
secretKey: accessProvider.secretKey
|
||||
},
|
||||
region: '',
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: 'ssl.tencentcloudapi.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SslClient(clientConfig)
|
||||
}
|
||||
|
||||
async execute ({ cert, props, context, logger }) {
|
||||
const { name, accessProvider } = props
|
||||
const certName = this.appendTimeSuffix(name || cert.domain)
|
||||
|
||||
const provider = this.getAccessProvider(accessProvider)
|
||||
const client = this.getClient(provider)
|
||||
|
||||
const params = {
|
||||
CertificatePublicKey: cert.crt,
|
||||
CertificatePrivateKey: cert.key,
|
||||
Alias: certName
|
||||
}
|
||||
const ret = await client.UploadCertificate(params)
|
||||
this.checkRet(ret)
|
||||
this.logger.info('证书上传成功:tencentCertId=', ret.CertificateId)
|
||||
context.tencentCertId = ret.CertificateId
|
||||
}
|
||||
|
||||
async rollback ({ cert, props, context }) {
|
||||
const { accessProvider } = props
|
||||
const provider = super.getAccessProvider(accessProvider)
|
||||
const client = this.getClient(provider)
|
||||
|
||||
const { tencentCertId } = context
|
||||
const params = {
|
||||
CertificateId: tencentCertId
|
||||
}
|
||||
const ret = await client.DeleteCertificate(params)
|
||||
this.checkRet(ret)
|
||||
this.logger.info('证书删除成功:DeleteResult=', ret.DeleteResult)
|
||||
delete context.tencentCertId
|
||||
}
|
||||
}
|
||||
109
packages/plugins/plugin-tencent/src/utils/util.k8s.client.js
Normal file
109
packages/plugins/plugin-tencent/src/utils/util.k8s.client.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import kubernetesClient from 'kubernetes-client'
|
||||
import { util } from '@certd/api'
|
||||
import Request from 'kubernetes-client/backends/request/index.js'
|
||||
import dns from 'dns'
|
||||
const { KubeConfig, Client } = kubernetesClient
|
||||
const logger = util.logger
|
||||
|
||||
export class K8sClient {
|
||||
constructor (kubeConfigStr) {
|
||||
this.kubeConfigStr = kubeConfigStr
|
||||
this.init()
|
||||
}
|
||||
|
||||
init () {
|
||||
const kubeconfig = new KubeConfig()
|
||||
kubeconfig.loadFromString(this.kubeConfigStr)
|
||||
const reqOpts = { kubeconfig, request: {} }
|
||||
if (this.lookup) {
|
||||
reqOpts.request.lookup = this.lookup
|
||||
}
|
||||
|
||||
const backend = new Request(reqOpts)
|
||||
this.client = new Client({ backend, version: '1.13' })
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param localRecords { [domain]:{ip:'xxx.xx.xxx'} }
|
||||
*/
|
||||
setLookup (localRecords) {
|
||||
this.lookup = (hostnameReq, options, callback) => {
|
||||
logger.info('custom lookup', hostnameReq, localRecords)
|
||||
if (localRecords[hostnameReq]) {
|
||||
logger.info('local record', hostnameReq, localRecords[hostnameReq])
|
||||
callback(null, localRecords[hostnameReq].ip, 4)
|
||||
} else {
|
||||
dns.lookup(hostnameReq, options, callback)
|
||||
}
|
||||
}
|
||||
this.init()
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 secret列表
|
||||
* @param opts = {namespace:default}
|
||||
* @returns secretsList
|
||||
*/
|
||||
async getSecret (opts = {}) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
const secrets = await this.client.api.v1.namespaces(namespace).secrets.get()
|
||||
return secrets
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Secret
|
||||
* @param opts {namespace:default, body:yamlStr}
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async createSecret (opts) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
const created = await this.client.api.v1.namespaces(namespace).secrets.post({
|
||||
body: opts.body
|
||||
})
|
||||
logger.info('new secrets:', created)
|
||||
return created
|
||||
}
|
||||
|
||||
async updateSecret (opts) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
const secretName = opts.secretName
|
||||
if (secretName == null) {
|
||||
throw new Error('secretName 不能为空')
|
||||
}
|
||||
return await this.client.api.v1.namespaces(namespace).secrets(secretName).put({
|
||||
body: opts.body
|
||||
})
|
||||
}
|
||||
|
||||
async patchSecret (opts) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
const secretName = opts.secretName
|
||||
if (secretName == null) {
|
||||
throw new Error('secretName 不能为空')
|
||||
}
|
||||
return await this.client.api.v1.namespaces(namespace).secrets(secretName).patch({
|
||||
body: opts.body
|
||||
})
|
||||
}
|
||||
|
||||
async getIngress (opts) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
const ingressName = opts.ingressName
|
||||
if (!ingressName) {
|
||||
throw new Error('ingressName 不能为空')
|
||||
}
|
||||
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).get()
|
||||
}
|
||||
|
||||
async patchIngress (opts) {
|
||||
const namespace = opts.namespace || 'default'
|
||||
const ingressName = opts.ingressName
|
||||
if (!ingressName) {
|
||||
throw new Error('ingressName 不能为空')
|
||||
}
|
||||
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).patch({
|
||||
body: opts.body
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import pkg from 'chai'
|
||||
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 = {
|
||||
type: 'dnspod',
|
||||
accessProvider: 'dnspod'
|
||||
}
|
||||
options.args = { forceCert: true }
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.certApply()
|
||||
expect(cert).ok
|
||||
expect(cert.crt).ok
|
||||
expect(cert.key).ok
|
||||
expect(cert.detail).ok
|
||||
expect(cert.expires).ok
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import pkg from 'chai'
|
||||
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 () {
|
||||
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 () {
|
||||
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
|
||||
|
||||
await dnsProvider.removeRecord({ fullRecord: '___certd___.__test__.certd.xyz', type: 'TXT', value: 'aaaa', record })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import pkg from 'chai'
|
||||
import { DeployCertToTencentCDN } from '../../src/plugins/deploy-to-cdn/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
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 = await certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn'])
|
||||
const context = {}
|
||||
const uploadPlugin = new UploadCertToTencent(options)
|
||||
const uploadOptions = {
|
||||
cert,
|
||||
props: { name: 'certd部署测试', accessProvider: 'tencent' },
|
||||
context
|
||||
}
|
||||
await uploadPlugin.doExecute(uploadOptions)
|
||||
|
||||
const deployPlugin = new DeployCertToTencentCDN(options)
|
||||
const deployOpts = {
|
||||
cert,
|
||||
props: { domainName: 'tentcent-certd.docmirror.cn', certName: 'certd部署测试', accessProvider: 'tencent' },
|
||||
context
|
||||
}
|
||||
await deployPlugin.doExecute(deployOpts)
|
||||
console.log('context:', context)
|
||||
expect(context.tencentCertId).ok
|
||||
|
||||
await uploadPlugin.doRollback(uploadOptions)
|
||||
})
|
||||
it('#execute-upload', async function () {
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.docmirror.cn']
|
||||
const plugin = new DeployCertToTencentCDN(options)
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const context = {}
|
||||
const deployOpts = {
|
||||
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
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,105 @@
|
||||
import pkg from 'chai'
|
||||
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/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(options)
|
||||
const props = {
|
||||
region: 'ap-guangzhou',
|
||||
domain: 'certd-test-no-sni.base.yonsz.net',
|
||||
accessProvider: 'tencent-yonsz'
|
||||
}
|
||||
const accessProvider = deployPlugin.getAccessProvider(props.accessProvider)
|
||||
const { region } = props
|
||||
const client = deployPlugin.getClient(accessProvider, region)
|
||||
|
||||
const ret = await deployPlugin.getCLBList(client, props)
|
||||
expect(ret.length > 0).ok
|
||||
console.log('clb count:', ret.length)
|
||||
})
|
||||
it('#execute-getListenerList', async function () {
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
options.cert.dnsProvider = 'tencent-yonsz'
|
||||
const deployPlugin = new DeployCertToTencentCLB(options)
|
||||
const props = {
|
||||
region: 'ap-guangzhou',
|
||||
domain: 'certd-test-no-sni.base.yonsz.net',
|
||||
accessProvider: 'tencent-yonsz',
|
||||
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.listenerId])
|
||||
expect(ret.length > 0).ok
|
||||
console.log('clb count:', ret.length, ret)
|
||||
})
|
||||
|
||||
it('#execute-no-sni-listenerId', async function () {
|
||||
this.timeout(10000)
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
options.cert.dnsProvider = 'tencent-yonsz'
|
||||
options.cert.email = 'xiaojunnuo@qq.com'
|
||||
options.cert.domains = ['*.docmirror.cn']
|
||||
const certd = new Certd(options)
|
||||
const cert = await certd.readCurrentCert()
|
||||
const deployPlugin = new DeployCertToTencentCLB(options)
|
||||
const context = {}
|
||||
const deployOpts = {
|
||||
cert,
|
||||
props: {
|
||||
region: 'ap-guangzhou',
|
||||
loadBalancerId: 'lb-59yhe5xo',
|
||||
listenerId: 'lbl-1vfwx8dq',
|
||||
accessProvider: 'tencent-yonsz'
|
||||
},
|
||||
context
|
||||
}
|
||||
const ret = await deployPlugin.doExecute(deployOpts)
|
||||
expect(ret).ok
|
||||
console.log('ret:', ret)
|
||||
|
||||
// 删除测试证书
|
||||
const uploadPlugin = new UploadCertToTencent(options)
|
||||
await uploadPlugin.doRollback(deployOpts)
|
||||
})
|
||||
|
||||
it('#execute-sni-listenerId', async function () {
|
||||
this.timeout(10000)
|
||||
const options = createOptions()
|
||||
options.args.test = false
|
||||
options.cert.dnsProvider = 'tencent-yonsz'
|
||||
const certd = new Certd(options)
|
||||
const cert = certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn'])
|
||||
const deployPlugin = new DeployCertToTencentCLB(options)
|
||||
const context = {}
|
||||
const deployOpts = {
|
||||
cert,
|
||||
props: {
|
||||
region: 'ap-guangzhou',
|
||||
loadBalancerId: 'lb-59yhe5xo',
|
||||
listenerId: 'lbl-akbyf5ac',
|
||||
domain: 'certd-test-sni.base.yonsz.net',
|
||||
accessProvider: 'tencent-yonsz'
|
||||
},
|
||||
context
|
||||
}
|
||||
const ret = await deployPlugin.doExecute(deployOpts)
|
||||
console.log('ret:', ret)
|
||||
expect(ret).ok
|
||||
// 删除测试证书
|
||||
const uploadPlugin = new UploadCertToTencent(options)
|
||||
await uploadPlugin.doRollback(deployOpts)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
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('DeployCertToTencentTKEIngress', 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: 'default' })
|
||||
|
||||
console.log('secrets:', secrets)
|
||||
})
|
||||
it('#execute', async function () {
|
||||
this.timeout(5000)
|
||||
const { options, deployOpts } = await getOptions()
|
||||
deployOpts.props.ingressName = 'ingress-base'
|
||||
deployOpts.props.secretName = 'cert---docmirror-cn'
|
||||
deployOpts.context.tencentCertId = 'hNUZJrZf'
|
||||
const plugin = new DeployCertToTencentTKEIngress(options)
|
||||
|
||||
const ret = await plugin.doExecute(deployOpts)
|
||||
console.log('sucess', ret)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
import pkg from 'chai'
|
||||
import { UploadCertToTencent } from '../../src/plugins/upload-to-tencent/index.js'
|
||||
import { Certd } from '@certd/certd'
|
||||
import { createOptions } from '../../../../../test/options.js'
|
||||
const { expect } = pkg
|
||||
describe('PluginUploadToTencent', function () {
|
||||
it('#execute', async function () {
|
||||
const options = createOptions()
|
||||
const plugin = new UploadCertToTencent(options)
|
||||
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 uploadOpts = {
|
||||
accessProviders: options.accessProviders,
|
||||
cert,
|
||||
props: { name: 'certd部署测试', accessProvider: 'tencent' },
|
||||
context
|
||||
}
|
||||
await plugin.doExecute(uploadOpts)
|
||||
console.log('context:', context)
|
||||
|
||||
await plugin.doRollback(uploadOpts)
|
||||
})
|
||||
})
|
||||
1858
packages/plugins/plugin-tencent/yarn-error.log
Normal file
1858
packages/plugins/plugin-tencent/yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
2922
packages/plugins/plugin-tencent/yarn.lock
Normal file
2922
packages/plugins/plugin-tencent/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user