mirror of
https://github.com/certd/certd.git
synced 2026-04-03 14:10:54 +08:00
Compare commits
99 Commits
client_syn
...
v0.1.15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -4,4 +4,13 @@
|
||||
out
|
||||
gen
|
||||
node_modules/
|
||||
packages/*/test/*-private.js
|
||||
/test/*.private.*
|
||||
|
||||
/*.log
|
||||
|
||||
/packages/ui/*/.idea
|
||||
|
||||
/packages/ui/*/node_modules
|
||||
|
||||
/packages/*/node_modules
|
||||
/packages/ui/certd-server/tmp/
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Greper
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
196
README.md
Normal file
196
README.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# 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. 创建任务项目
|
||||
```
|
||||
mkdir certd-run # 项目名称可以任意命名
|
||||
cd certd-run -y
|
||||
npm install @certd/executor -s --production
|
||||
```
|
||||
|
||||
3. 创建index.js
|
||||
|
||||
参数配置分几个部分
|
||||
args: 运行时参数
|
||||
accessProviders: 授权提供者,提供dns验证与部署任务的授权
|
||||
cert: 证书申请的配置
|
||||
deploy: 证书部署流程
|
||||
|
||||
```js
|
||||
import { Executor } from '@certd/executor'
|
||||
const options = {
|
||||
args: { // 运行时参数
|
||||
forceDeploy: true,
|
||||
},
|
||||
accessProviders: { //授权提供者
|
||||
aliyun: { // 阿里云accessKey,用于dns验证和上传证书到阿里云,并部署到cdn
|
||||
providerType: 'aliyun',
|
||||
accessKeyId: 'Your accessKeyId',
|
||||
accessKeySecret: 'Your accessKeySecret'
|
||||
},
|
||||
},
|
||||
cert: { //免费证书申请配置
|
||||
domains: [ //可以在一张证书上绑定多个域名(前提是他们的验证方式要一样,目前仅支持dns验证)
|
||||
'*.yourdomain.com',
|
||||
'*.test.yourdomain.com',
|
||||
'yourdomain.com'
|
||||
],
|
||||
email: 'Your email',
|
||||
dnsProvider: 'aliyun', //上方accessProviders里面配置的
|
||||
csrInfo: { //证书csr信息
|
||||
country: 'CN',
|
||||
state: 'GuangDong',
|
||||
locality: 'ShengZhen',
|
||||
organization: 'Your company Org.',
|
||||
organizationUnit: 'IT Department',
|
||||
emailAddress: 'Your email'
|
||||
}
|
||||
},
|
||||
deploy: [ //部署流程配置,数组,可以配置多条流程
|
||||
{
|
||||
deployName: '流程1-部署到阿里云CDN',
|
||||
tasks: [ //流程任务,一个流程下可以包含多个部署任务,并且将按顺序执行
|
||||
{ //任务1
|
||||
taskName: '上传到阿里云', //任务名称
|
||||
type: 'uploadCertToAliyun', //任务插件名称
|
||||
props: { //任务插件参数
|
||||
accessProvider: 'aliyun'
|
||||
}
|
||||
},
|
||||
{ // 任务2
|
||||
taskName: '部署证书到CDN',
|
||||
type: 'deployCertToAliyunCDN', //任务插件名称
|
||||
props:{
|
||||
domainName: 'your cdn domain 全称', //cdn域名全称
|
||||
certName: 'certd自动部署',//证书名称前缀
|
||||
accessProvider: 'aliyun'
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
const executor = new Executor()
|
||||
await executor.run(options)
|
||||
```
|
||||
|
||||
4. 运行
|
||||
```
|
||||
node index.js
|
||||
```
|
||||
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集成与自动续期重新部署
|
||||
集成前,将以上代码提交到内网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
|
||||
|
||||
|
||||
### 更多部署插件
|
||||
等你来提需求
|
||||
6
lerna.json
Normal file
6
lerna.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"packages": [
|
||||
"packages/*/*"
|
||||
],
|
||||
"version": "0.1.15"
|
||||
}
|
||||
22168
package-lock.json
generated
Normal file
22168
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
package.json
Normal file
16
package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "root",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"lerna": "^3.18.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "lerna bootstrap --hoist",
|
||||
"i-all": "lerna link && lerna exec npm install "
|
||||
},
|
||||
"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.15",
|
||||
"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()
|
||||
42
packages/core/api/src/dns-provider/abstract-dns-provider.js
Normal file
42
packages/core/api/src/dns-provider/abstract-dns-provider.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import _ from 'lodash-es'
|
||||
import logger from '../utils/util.log.js'
|
||||
export class AbstractDnsProvider {
|
||||
constructor ({ accessProviders }) {
|
||||
this.logger = logger
|
||||
this.accessProviders = 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) {
|
||||
if (typeof accessProvider === 'string' && accessProviders) {
|
||||
accessProvider = accessProviders[accessProvider]
|
||||
}
|
||||
return accessProvider
|
||||
}
|
||||
}
|
||||
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()
|
||||
77
packages/core/api/src/plugin/abstract-plugin.js
Normal file
77
packages/core/api/src/plugin/abstract-plugin.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import fs from 'fs'
|
||||
import logger from '../utils/util.log.js'
|
||||
import dayjs from 'dayjs'
|
||||
import Sleep from '../utils/util.sleep.js'
|
||||
|
||||
export class AbstractPlugin {
|
||||
constructor (options) {
|
||||
if (options == null) {
|
||||
throw new Error('插件安装失败:参数不允许为空')
|
||||
}
|
||||
const { accessProviders } = options
|
||||
this.logger = logger
|
||||
this.accessProviders = 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) {
|
||||
if (typeof accessProvider === 'string' && accessProviders) {
|
||||
accessProvider = accessProviders[accessProvider]
|
||||
}
|
||||
return accessProvider
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
7
packages/core/api/src/utils/index.js
Normal file
7
packages/core/api/src/utils/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import logger from './util.log.js'
|
||||
import path from './util.path.js'
|
||||
import { request } from './util.request.js'
|
||||
import sleep from './util.sleep.js'
|
||||
export const util = {
|
||||
logger, path, request, sleep
|
||||
}
|
||||
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.15",
|
||||
"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.6",
|
||||
"@certd/api": "^0.1.15",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
3960
packages/core/executor/package-lock.json
generated
Normal file
3960
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.15",
|
||||
"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.15",
|
||||
"@certd/certd": "^0.1.15",
|
||||
"dayjs": "^1.9.7",
|
||||
"lodash-es": "^4.17.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/plugin-aliyun": "^0.1.15",
|
||||
"@certd/plugin-host": "^0.1.15",
|
||||
"@certd/plugin-tencent": "^0.1.15",
|
||||
"@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()]
|
||||
}
|
||||
163
packages/core/executor/src/index.js
Normal file
163
packages/core/executor/src/index.js
Normal file
@@ -0,0 +1,163 @@
|
||||
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 {
|
||||
options = _.merge(createDefaultOptions(), options)
|
||||
return await this.doRun(options)
|
||||
} catch (e) {
|
||||
logger.error('任务执行出错:' + e.message, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
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.forceDeploy && !options.args.forceRedeploy) {
|
||||
// 且不需要强制运行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({ })
|
||||
const returnData = {
|
||||
cert,
|
||||
context,
|
||||
result
|
||||
}
|
||||
if (result.status === 'error' && options.args.doNotThrowError === false) {
|
||||
throw new Error(result.remark)
|
||||
}
|
||||
return returnData
|
||||
}
|
||||
|
||||
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: '流程禁用' } })
|
||||
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('')
|
||||
}
|
||||
}
|
||||
94
packages/core/executor/src/trace.js
Normal file
94
packages/core/executor/src/trace.js
Normal file
@@ -0,0 +1,94 @@
|
||||
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' })
|
||||
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
|
||||
// }
|
||||
}
|
||||
3136
packages/core/executor/yarn.lock
Normal file
3136
packages/core/executor/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
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.15",
|
||||
"description": "",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@alicloud/pop-core": "^1.7.10",
|
||||
"@certd/api": "^0.1.15",
|
||||
"dayjs": "^1.9.7",
|
||||
"lodash-es": "^4.17.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/certd": "^0.1.15",
|
||||
"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.15",
|
||||
"description": "",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@certd/api": "^0.1.15",
|
||||
"dayjs": "^1.9.7",
|
||||
"lodash-es": "^4.17.20",
|
||||
"ssh2": "^0.8.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/certd": "^0.1.15",
|
||||
"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: '登录密码' },
|
||||
publicKey: {
|
||||
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 }) {
|
||||
|
||||
}
|
||||
}
|
||||
128
packages/plugins/plugin-host/src/plugins/ssh.js
Normal file
128
packages/plugins/plugin-host/src/plugins/ssh.js
Normal file
@@ -0,0 +1,128 @@
|
||||
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 }) {
|
||||
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))
|
||||
await this.exec({ conn, cmd: 'mkdir ' + 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')
|
||||
}
|
||||
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}, signal:${signal} `)
|
||||
if (code === 0) {
|
||||
resolve(data.toString())
|
||||
} else {
|
||||
reject(data.toString())
|
||||
}
|
||||
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
|
||||
stream.close()
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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,81 @@
|
||||
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
|
||||
}
|
||||
},
|
||||
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,28 @@
|
||||
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))
|
||||
|
||||
await plugin.doRollback(uploadOpts)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
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.15",
|
||||
"description": "",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@certd/api": "^0.1.15",
|
||||
"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.15",
|
||||
"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: '该项必填' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
packages/plugins/plugin-tencent/src/dns-providers/dnspod.js
Normal file
94
packages/plugins/plugin-tencent/src/dns-providers/dnspod.js
Normal file
@@ -0,0 +1,94 @@
|
||||
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) {
|
||||
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 || ret.status.code !== '1') {
|
||||
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
|
||||
}
|
||||
})
|
||||
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,173 @@
|
||||
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: '支持多个(传入数组)'
|
||||
},
|
||||
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 } })
|
||||
}
|
||||
await this.patchCertSecret({ 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 patchCertSecret ({ 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 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,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
16
packages/ui/certd-server/.eslintrc.cjs
Normal file
16
packages/ui/certd-server/.eslintrc.cjs
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: '2020'
|
||||
},
|
||||
parser: 'babel-eslint',
|
||||
extends: ['standard'],
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
|
||||
}
|
||||
}
|
||||
53
packages/ui/certd-server/app.js
Normal file
53
packages/ui/certd-server/app.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import Koa from 'koa'
|
||||
import json from 'koa-json'
|
||||
import onerror from 'koa-onerror'
|
||||
import bodyparser from 'koa-bodyparser'
|
||||
import logger from 'koa-logger'
|
||||
import Static from 'koa-static'
|
||||
import fs from 'fs'
|
||||
import _ from 'lodash-es'
|
||||
|
||||
const app = new Koa()
|
||||
|
||||
// error handler
|
||||
onerror(app)
|
||||
|
||||
// middlewares
|
||||
app.use(bodyparser({
|
||||
enableTypes: ['json', 'form', 'text']
|
||||
}))
|
||||
app.use(json())
|
||||
app.use(logger())
|
||||
|
||||
app.use(Static(new URL('public', import.meta.url).pathname))
|
||||
|
||||
// logger
|
||||
app.use(async (ctx, next) => {
|
||||
const start = new Date()
|
||||
await next()
|
||||
const ms = new Date() - start
|
||||
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
|
||||
})
|
||||
|
||||
console.log('url', import.meta.url)
|
||||
|
||||
// routes
|
||||
const files = fs.readdirSync(new URL('controllers/', import.meta.url))
|
||||
// 过滤出.js文件:
|
||||
const jsFiles = files.filter((f) => {
|
||||
return f.endsWith('.js')
|
||||
})
|
||||
|
||||
_.forEach(jsFiles, async item => {
|
||||
let mapping = await import(new URL('controllers/' + item, import.meta.url))
|
||||
mapping = mapping.default
|
||||
app.use(mapping.routes(), mapping.allowedMethods())
|
||||
})
|
||||
|
||||
// error-handling
|
||||
app.on('error', (err, ctx) => {
|
||||
console.error('server error', err, ctx)
|
||||
})
|
||||
|
||||
console.log('http://localhost:3000/')
|
||||
export default app
|
||||
93
packages/ui/certd-server/bin/www.js
Normal file
93
packages/ui/certd-server/bin/www.js
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
import app from '../app.js';
|
||||
import debuger from 'debug'
|
||||
const debug = debuger('demo:serer')
|
||||
// require('debug')('demo:server');
|
||||
import http from 'http';
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
// app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
var server = http.createServer(app.callback());
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
}
|
||||
|
||||
19
packages/ui/certd-server/controllers/access-providers.js
Normal file
19
packages/ui/certd-server/controllers/access-providers.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Router from 'koa-router'
|
||||
import { accessProviderRegistry } from '@certd/api'
|
||||
import DefaultAccessProviders from '@certd/access-providers'
|
||||
import _ from 'lodash-es'
|
||||
import { Ret } from '../models/Ret.js'
|
||||
const router = Router()
|
||||
router.prefix('/access-providers')
|
||||
|
||||
DefaultAccessProviders.install()
|
||||
|
||||
router.get('/list', function (ctx, next) {
|
||||
const list = []
|
||||
_.forEach(accessProviderRegistry.collection, item => {
|
||||
list.push(item.define())
|
||||
})
|
||||
ctx.body = Ret.success(list)
|
||||
})
|
||||
|
||||
export default router
|
||||
19
packages/ui/certd-server/controllers/dns-providers.js
Normal file
19
packages/ui/certd-server/controllers/dns-providers.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Router from 'koa-router'
|
||||
import { dnsProviderRegistry } from '@certd/api'
|
||||
import DefaultDnsProviders from '@certd/dns-providers'
|
||||
import _ from 'lodash-es'
|
||||
import { Ret } from '../models/Ret.js'
|
||||
const router = Router()
|
||||
router.prefix('/dns-providers')
|
||||
|
||||
DefaultDnsProviders.install()
|
||||
|
||||
router.get('/list', function (ctx, next) {
|
||||
const list = []
|
||||
_.forEach(dnsProviderRegistry.collection, item => {
|
||||
list.push(item.define())
|
||||
})
|
||||
ctx.body = Ret.success(list)
|
||||
})
|
||||
|
||||
export default router
|
||||
23
packages/ui/certd-server/controllers/exports.js
Normal file
23
packages/ui/certd-server/controllers/exports.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Router from 'koa-router'
|
||||
import fs from 'fs'
|
||||
import exportsService from '../service/exports-service.js'
|
||||
|
||||
const router = Router()
|
||||
router.prefix('/exports')
|
||||
|
||||
router.post('/toZip', async function (ctx, next) {
|
||||
// const request = ctx.request
|
||||
// const query = request.query
|
||||
const body = ctx.request.body
|
||||
// const req_queryString = request.queryString
|
||||
const { zipPath, fileName } = await exportsService.exportsToZip(body.options, 'certd-run')
|
||||
|
||||
console.log('zipFile', zipPath)
|
||||
ctx.set('Content-disposition', 'attachment;filename=' + fileName)
|
||||
ctx.set('Content-Type', 'application/zip')
|
||||
ctx.body = fs.createReadStream(zipPath)
|
||||
//
|
||||
// // ctx.body = Ret.success(zipPath)
|
||||
})
|
||||
|
||||
export default router
|
||||
10
packages/ui/certd-server/controllers/index.js
Normal file
10
packages/ui/certd-server/controllers/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import Router from 'koa-router'
|
||||
const router = Router()
|
||||
|
||||
router.get('/', async (ctx, next) => {
|
||||
await ctx.render('index', {
|
||||
title: 'Hello CertD!'
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
||||
19
packages/ui/certd-server/controllers/plugins.js
Normal file
19
packages/ui/certd-server/controllers/plugins.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Router from 'koa-router'
|
||||
import { pluginRegistry } from '@certd/api'
|
||||
import DefaultPlugins from '@certd/plugins'
|
||||
import _ from 'lodash-es'
|
||||
import { Ret } from '../models/Ret.js'
|
||||
const router = Router()
|
||||
router.prefix('/plugins')
|
||||
|
||||
DefaultPlugins.install()
|
||||
|
||||
router.get('/list', function (ctx, next) {
|
||||
const list = []
|
||||
_.forEach(pluginRegistry.collection, item => {
|
||||
list.push(item.define())
|
||||
})
|
||||
ctx.body = Ret.success(list)
|
||||
})
|
||||
|
||||
export default router
|
||||
15
packages/ui/certd-server/models/Ret.js
Normal file
15
packages/ui/certd-server/models/Ret.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export class Ret {
|
||||
constructor (code = 0, msg, data) {
|
||||
this.code = code
|
||||
this.msg = msg
|
||||
this.data = data
|
||||
}
|
||||
|
||||
static success (data) {
|
||||
return new Ret(0, '', data)
|
||||
}
|
||||
|
||||
static error (msg) {
|
||||
return new Ret(1, msg)
|
||||
}
|
||||
}
|
||||
5694
packages/ui/certd-server/package-lock.json
generated
Normal file
5694
packages/ui/certd-server/package-lock.json
generated
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