mirror of
https://github.com/certd/certd.git
synced 2026-04-15 05:00:52 +08:00
Compare commits
2 Commits
v0.2.0
...
server_syn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b828b3f34 | ||
|
|
fbde7cbd93 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,9 +1,18 @@
|
|||||||
# IntelliJ project files
|
# IntelliJ project files
|
||||||
|
.vscode/
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
|
/.idea/
|
||||||
|
*/**/dist
|
||||||
|
*/**/pnpm-lock.yaml
|
||||||
|
*/**/stats.html
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
out
|
out
|
||||||
gen
|
gen
|
||||||
node_modules/
|
|
||||||
/test/*.private.*
|
/test/*.private.*
|
||||||
|
|
||||||
/*.log
|
/*.log
|
||||||
@@ -18,3 +27,6 @@ node_modules/
|
|||||||
/other
|
/other
|
||||||
/dev-sidecar-test
|
/dev-sidecar-test
|
||||||
/packages/core/certd/yarn.lock
|
/packages/core/certd/yarn.lock
|
||||||
|
/packages/test
|
||||||
|
/test/own
|
||||||
|
/pnpm-lock.yaml
|
||||||
|
|||||||
139
README.md
139
README.md
@@ -1,139 +0,0 @@
|
|||||||
# CertD
|
|
||||||
|
|
||||||
CertD 是一个帮助你全自动申请和部署SSL证书的工具。
|
|
||||||
后缀D取自linux守护进程的命名风格,意为证书守护进程。
|
|
||||||
|
|
||||||
## 特性
|
|
||||||
本项目不仅支持证书申请过程自动化,还可以自动化部署证书,让你的证书永不过期。
|
|
||||||
|
|
||||||
* 全自动申请证书
|
|
||||||
* 全自动部署证书(目前支持服务器上传部署、阿里云、腾讯云等)
|
|
||||||
* 可与CI/DI工具结合使用
|
|
||||||
|
|
||||||
## 免费证书申请说明
|
|
||||||
* 本项目ssl证书提供商为letencrypt
|
|
||||||
* 申请过程遵循acme协议
|
|
||||||
* 需要验证域名所有权,一般有两种方式(目前本项目仅支持dns-01)
|
|
||||||
* http-01: 在网站根目录下放置一份txt文件
|
|
||||||
* dns-01: 需要给域名添加txt解析记录,泛域名只能用这种方式
|
|
||||||
* 证书续期:
|
|
||||||
* 实际上acme并没有续期概念。
|
|
||||||
* 我们所说的续期,其实就是按照全套流程重新申请一份新证书。
|
|
||||||
* 免费证书过期时间90天,以后可能还会缩短,所以自动化部署必不可少
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
本案例演示,如何配置自动申请证书,并部署到阿里云CDN,然后快要到期前自动更新证书并重新部署
|
|
||||||
|
|
||||||
|
|
||||||
1. 环境准备
|
|
||||||
安装[nodejs](https://nodejs.org/zh-cn/)
|
|
||||||
|
|
||||||
|
|
||||||
2. 生成node项目
|
|
||||||
|
|
||||||
通过ui生成: https://certd.docmirror.cn/
|
|
||||||
|
|
||||||
开始生成证书,先填写域名,支持将多个域名打到一个证书上
|
|
||||||

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

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

|
|
||||||
|
|
||||||
配置好之后,点击导出按钮,导出一个node项目包
|
|
||||||
|
|
||||||
4. 运行
|
|
||||||
将导出的压缩包解压,然后执行如下命令,即可开始申请证书并部署
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
npm run certd
|
|
||||||
```
|
|
||||||
5. 执行效果
|
|
||||||
生成的证书默认会存储在 `${home}/.certd/${email}/certs/${domain}/current`目录下
|
|
||||||
```
|
|
||||||
[2021-01-08T16:15:04.681] [INFO] certd - 任务完成
|
|
||||||
[2021-01-08T16:15:04.681] [INFO] certd - ---------------------------任务结果总览--------------------------
|
|
||||||
[2021-01-08T16:15:04.682] [INFO] certd - 【更新证书】--------------------------------------- [success]
|
|
||||||
证书申请成功
|
|
||||||
[2021-01-08T16:15:04.682] [INFO] certd - 【流程1-部署到阿里云CDN】---------------------------- [success] 执行成功
|
|
||||||
[2021-01-08T16:15:04.682] [INFO] certd - └【上传到阿里云】-------------------------------- [success] 执行成功
|
|
||||||
[2021-01-08T16:15:04.682] [INFO] certd - └【部署证书到CDN】------------------------------- [success] 执行成功
|
|
||||||
```
|
|
||||||
6. 证书续期
|
|
||||||
实际上没有证书续期的概念,只有重新生成一份新的证书,然后重新部署证书
|
|
||||||
所以每天定时运行即可,当证书过期日前20天时,会重新申请新的证书,然后执行部署任务。
|
|
||||||
|
|
||||||
7. 其他说明
|
|
||||||
证书的部署任务执行后会记录执行结果,已经成功过的不会重复执行
|
|
||||||
所以当你临时需要将证书部署到其他地方时,直接追加部署任务,然后再次运行即可
|
|
||||||
|
|
||||||
## CI/DI集成与自动续期重新部署
|
|
||||||
集成前,将以上导出的node项目提交到内网git仓库,或者私有git仓库(由于包含敏感信息,不要提交到公开git仓库)
|
|
||||||
|
|
||||||
### jenkins任务
|
|
||||||
1. 创建任务
|
|
||||||
选择构建自由风格的任务
|
|
||||||
|
|
||||||
2. 配置git
|
|
||||||
配置cert-run的git地址
|
|
||||||
|
|
||||||
3. 构建触发器
|
|
||||||
配置 `H 3 * * *` ,每天凌晨3点-4点执行一次
|
|
||||||
|
|
||||||
4. 构建环境
|
|
||||||
勾选 `Provide Node & npm bin/ folder to PATH`,提供nodejs运行环境
|
|
||||||
如果没有此选项,需要jenkins安装`nodejs`插件
|
|
||||||
|
|
||||||
5. 构建
|
|
||||||
执行shell
|
|
||||||
```
|
|
||||||
npm install --production #执行过一次之后,就可以注释掉,加快执行速度
|
|
||||||
npm run post
|
|
||||||
```
|
|
||||||
6. 构建后操作
|
|
||||||
邮件通知
|
|
||||||
配置你的邮箱地址,可以在执行失败时收到邮件通知。
|
|
||||||
|
|
||||||
|
|
||||||
## API
|
|
||||||
先列个提纲,待完善
|
|
||||||
|
|
||||||
参数示例参考:https://gitee.com/certd/certd/blob/master/test/options.js
|
|
||||||
|
|
||||||
### 授权提供者
|
|
||||||
用于dns验证接口调用
|
|
||||||
#### aliyun
|
|
||||||
|
|
||||||
#### dnspod
|
|
||||||
|
|
||||||
### deploy插件
|
|
||||||
部署任务插件
|
|
||||||
#### 阿里云
|
|
||||||
##### 上传到阿里云
|
|
||||||
type = uploadCertToAliyun
|
|
||||||
##### 部署到阿里云DNS
|
|
||||||
type = deployCertToAliyunCDN
|
|
||||||
|
|
||||||
##### 部署到阿里云CLB
|
|
||||||
type = deployCertToAliyunCLB
|
|
||||||
|
|
||||||
#### 腾讯云
|
|
||||||
##### 上传到腾讯云
|
|
||||||
type = uploadCertToTencent
|
|
||||||
|
|
||||||
##### 部署到腾讯云DNS
|
|
||||||
type = deployCertToTencentDNS
|
|
||||||
|
|
||||||
##### 部署到腾讯云CLB
|
|
||||||
type = deployCertToTencentCLB
|
|
||||||
|
|
||||||
##### 部署到腾讯云TKE-ingress
|
|
||||||
type = deployCertToTencentTKEIngress
|
|
||||||
|
|
||||||
|
|
||||||
### 更多部署插件
|
|
||||||
等你来提需求
|
|
||||||
BIN
doc/step1.png
BIN
doc/step1.png
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
BIN
doc/step2.png
BIN
doc/step2.png
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
BIN
doc/step3.png
BIN
doc/step3.png
Binary file not shown.
|
Before Width: | Height: | Size: 92 KiB |
BIN
doc/tasks.png
BIN
doc/tasks.png
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"packages": [
|
|
||||||
"packages/*/*"
|
|
||||||
],
|
|
||||||
"version": "0.2.0"
|
|
||||||
}
|
|
||||||
22168
package-lock.json
generated
22168
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"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
2468
packages/core/api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@certd/api",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import { Registry } from '../registry/registry.js'
|
|
||||||
export const accessProviderRegistry = new Registry()
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import _ from 'lodash-es'
|
|
||||||
import logger from '../utils/util.log.js'
|
|
||||||
import commonUtil from '../utils/util.common.js'
|
|
||||||
export class AbstractDnsProvider {
|
|
||||||
constructor ({ accessProviders }) {
|
|
||||||
this.logger = logger
|
|
||||||
this.accessProviders = commonUtil.arrayToMap(accessProviders)
|
|
||||||
}
|
|
||||||
|
|
||||||
async createRecord ({ fullRecord, type, value }) {
|
|
||||||
throw new Error('请实现 createRecord 方法')
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeRecord ({ fullRecord, type, value, record }) {
|
|
||||||
throw new Error('请实现 removeRecord 方法')
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDomainList () {
|
|
||||||
throw new Error('请实现 getDomainList 方法')
|
|
||||||
}
|
|
||||||
|
|
||||||
async matchDomain (dnsRecord, domainPropName) {
|
|
||||||
const list = await this.getDomainList()
|
|
||||||
let domain = null
|
|
||||||
for (const item of list) {
|
|
||||||
if (_.endsWith(dnsRecord, item[domainPropName])) {
|
|
||||||
domain = item
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!domain) {
|
|
||||||
throw new Error('找不到域名,请检查域名是否正确:' + dnsRecord)
|
|
||||||
}
|
|
||||||
return domain
|
|
||||||
}
|
|
||||||
|
|
||||||
getAccessProvider (accessProvider, accessProviders = this.accessProviders) {
|
|
||||||
let access = accessProvider
|
|
||||||
if (typeof accessProvider === 'string' && accessProviders) {
|
|
||||||
access = accessProviders[accessProvider]
|
|
||||||
}
|
|
||||||
if (access == null) {
|
|
||||||
throw new Error(`accessProvider :${accessProvider}不存在`)
|
|
||||||
}
|
|
||||||
return access
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { Registry } from '../registry/registry.js'
|
|
||||||
export { AbstractDnsProvider } from './abstract-dns-provider.js'
|
|
||||||
|
|
||||||
export const dnsProviderRegistry = new Registry()
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import logger from '../utils/util.log.js'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import Sleep from '../utils/util.sleep.js'
|
|
||||||
import commonUtil from '../utils/util.common.js'
|
|
||||||
export class AbstractPlugin {
|
|
||||||
constructor (options) {
|
|
||||||
if (options == null) {
|
|
||||||
throw new Error('插件安装失败:参数不允许为空')
|
|
||||||
}
|
|
||||||
const { accessProviders } = options
|
|
||||||
this.logger = logger
|
|
||||||
this.accessProviders = commonUtil.arrayToMap(accessProviders)
|
|
||||||
}
|
|
||||||
|
|
||||||
appendTimeSuffix (name) {
|
|
||||||
if (name == null) {
|
|
||||||
name = 'certd'
|
|
||||||
}
|
|
||||||
return name + '-' + dayjs().format('YYYYMMDD-HHmmss')
|
|
||||||
}
|
|
||||||
|
|
||||||
async executeFromContextFile (options = {}) {
|
|
||||||
const { contextPath } = options
|
|
||||||
const contextJson = fs.readFileSync(contextPath)
|
|
||||||
const context = JSON.parse(contextJson)
|
|
||||||
options.context = context
|
|
||||||
await this.doExecute(options)
|
|
||||||
fs.writeFileSync(JSON.stringify(context))
|
|
||||||
}
|
|
||||||
|
|
||||||
async doExecute (options) {
|
|
||||||
try {
|
|
||||||
return await this.execute(options)
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('插件执行出错:', e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行
|
|
||||||
* @param options
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async execute (options) {
|
|
||||||
console.error('请实现此方法,context:', options.context)
|
|
||||||
}
|
|
||||||
|
|
||||||
async doRollback (options) {
|
|
||||||
try {
|
|
||||||
return await this.rollback(options)
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('插件rollback出错:', e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回退,用于单元测试
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
async rollback (options) {
|
|
||||||
console.error('请实现此方法,rollback:', options.context)
|
|
||||||
}
|
|
||||||
|
|
||||||
getAccessProvider (accessProvider, accessProviders = this.accessProviders) {
|
|
||||||
let access = accessProvider
|
|
||||||
if (typeof accessProvider === 'string' && accessProviders) {
|
|
||||||
access = accessProviders[accessProvider]
|
|
||||||
}
|
|
||||||
if (access == null) {
|
|
||||||
throw new Error(`accessProvider :${accessProvider}不存在`)
|
|
||||||
}
|
|
||||||
return access
|
|
||||||
}
|
|
||||||
|
|
||||||
async sleep (time) {
|
|
||||||
await Sleep(time)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { Registry } from '../registry/registry.js'
|
|
||||||
export { AbstractPlugin } from './abstract-plugin.js'
|
|
||||||
export const pluginRegistry = new Registry()
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
export class Store {
|
|
||||||
set (key, value) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
get (key) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
buildKey (...keyItem) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
linkExists (linkPath) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
link (targetPath, linkPath) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
unlink (linkPath) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全路径
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
getActualKey (key) {
|
|
||||||
// return 前缀+key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import logger from './util.log.js'
|
|
||||||
import path from './util.path.js'
|
|
||||||
import { request } from './util.request.js'
|
|
||||||
import sleep from './util.sleep.js'
|
|
||||||
import common from './util.common.js'
|
|
||||||
export const util = {
|
|
||||||
logger, path, request, sleep, common
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import _ from 'lodash-es'
|
|
||||||
export default {
|
|
||||||
arrayToMap (array) {
|
|
||||||
if (!array) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
if (!_.isArray(array)) {
|
|
||||||
return array
|
|
||||||
}
|
|
||||||
const map = {}
|
|
||||||
for (const item of array) {
|
|
||||||
if (item.key) {
|
|
||||||
map[item.key] = item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map
|
|
||||||
},
|
|
||||||
mapToArray (map) {
|
|
||||||
if (!map) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
if (_.isArray(map)) {
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
const array = []
|
|
||||||
for (const key in map) {
|
|
||||||
const item = map[key]
|
|
||||||
item.key = key
|
|
||||||
array.push(item)
|
|
||||||
}
|
|
||||||
return array
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import log4js from 'log4js'
|
|
||||||
log4js.configure({
|
|
||||||
appenders: { std: { type: 'stdout' } },
|
|
||||||
categories: { default: { appenders: ['std'], level: 'info' } }
|
|
||||||
})
|
|
||||||
const logger = log4js.getLogger('certd')
|
|
||||||
export default logger
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import path from 'path'
|
|
||||||
|
|
||||||
function getUserBasePath () {
|
|
||||||
const userHome = process.env.USERPROFILE || process.env.HOME
|
|
||||||
return path.resolve(userHome, './.certd')
|
|
||||||
}
|
|
||||||
export default {
|
|
||||||
getUserBasePath
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export default function (timeout) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve()
|
|
||||||
}, timeout)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "standard",
|
|
||||||
"env": {
|
|
||||||
"mocha": true
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["*.test.js", "*.spec.js"],
|
|
||||||
"rules": {
|
|
||||||
"no-unused-expressions": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
7
packages/core/certd/.gitignore
vendored
7
packages/core/certd/.gitignore
vendored
@@ -1,7 +0,0 @@
|
|||||||
.vscode/
|
|
||||||
node_modules/
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
yarn.lock
|
|
||||||
package-lock.json
|
|
||||||
/.idea/
|
|
||||||
2493
packages/core/certd/package-lock.json
generated
2493
packages/core/certd/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@certd/certd",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"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.2.0",
|
|
||||||
"@certd/api": "^0.2.0",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
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))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "standard",
|
|
||||||
"env": {
|
|
||||||
"mocha": true
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["*.test.js", "*.spec.js"],
|
|
||||||
"rules": {
|
|
||||||
"no-unused-expressions": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
5696
packages/core/executor/package-lock.json
generated
5696
packages/core/executor/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@certd/executor",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"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.2.0",
|
|
||||||
"@certd/certd": "^0.2.0",
|
|
||||||
"dayjs": "^1.9.7",
|
|
||||||
"lodash-es": "^4.17.20"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@certd/plugin-aliyun": "^0.2.0",
|
|
||||||
"@certd/plugin-host": "^0.2.0",
|
|
||||||
"@certd/plugin-tencent": "^0.2.0",
|
|
||||||
"@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"
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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()]
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
import { Certd } from '@certd/certd'
|
|
||||||
import { pluginRegistry, util } from '@certd/api'
|
|
||||||
import _ from 'lodash-es'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { Trace } from './trace.js'
|
|
||||||
const logger = util.logger
|
|
||||||
|
|
||||||
function createDefaultOptions () {
|
|
||||||
return {
|
|
||||||
args: {
|
|
||||||
forceCert: false,
|
|
||||||
forceDeploy: true,
|
|
||||||
forceRedeploy: false,
|
|
||||||
doNotThrowError: false // 部署流程执行有错误时,不抛异常,此时整个任务执行完毕后,可以返回结果,你可以在返回结果中处理
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export class Executor {
|
|
||||||
constructor () {
|
|
||||||
this.trace = new Trace()
|
|
||||||
}
|
|
||||||
|
|
||||||
async run (options) {
|
|
||||||
logger.info('------------------- Cert-D ---------------------')
|
|
||||||
try {
|
|
||||||
this.transfer(options)
|
|
||||||
options = _.merge(createDefaultOptions(), options)
|
|
||||||
return await this.doRun(options)
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('任务执行出错', e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transfer (options) {
|
|
||||||
const providers = options.accessProviders
|
|
||||||
if (_.isArray(providers)) {
|
|
||||||
const map = {}
|
|
||||||
for (const provider of providers) {
|
|
||||||
if (provider.key) {
|
|
||||||
map[provider.key] = provider
|
|
||||||
}
|
|
||||||
}
|
|
||||||
options.accessProviders = map
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async doRun (options) {
|
|
||||||
// 申请证书
|
|
||||||
logger.info('任务开始')
|
|
||||||
const certd = new Certd(options)
|
|
||||||
const cert = await this.runCertd(certd)
|
|
||||||
if (cert == null) {
|
|
||||||
throw new Error('申请证书失败')
|
|
||||||
}
|
|
||||||
logger.info('证书保存路径:', cert.certDir)
|
|
||||||
|
|
||||||
logger.info('----------------------')
|
|
||||||
if (!cert.isNew) {
|
|
||||||
// 如果没有更新
|
|
||||||
if (options.args.forceRedeploy) {
|
|
||||||
// 强制重新部署,清空保存的状态
|
|
||||||
await certd.certStore.setCurrentFile('context.json', '{}')
|
|
||||||
} else if (!options.args.forceDeploy) {
|
|
||||||
// 且不需要强制deploy
|
|
||||||
logger.info('证书无更新,无需重新部署')
|
|
||||||
logger.info('任务完成')
|
|
||||||
return { cert }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 读取上次执行进度
|
|
||||||
let context = {}
|
|
||||||
const contextJson = await certd.certStore.getCurrentFile('context.json')
|
|
||||||
if (contextJson) {
|
|
||||||
context = JSON.parse(contextJson)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.certIsNew = !!cert.isNew
|
|
||||||
|
|
||||||
const trace = new Trace(context)
|
|
||||||
const resultTrace = trace.getInstance({ type: 'result' })
|
|
||||||
// 运行部署任务
|
|
||||||
try {
|
|
||||||
await this.runDeploys({ options, cert, context, trace })
|
|
||||||
} finally {
|
|
||||||
await certd.certStore.setCurrentFile('context.json', JSON.stringify(context))
|
|
||||||
}
|
|
||||||
logger.info('任务完成')
|
|
||||||
trace.print()
|
|
||||||
const result = resultTrace.get({ })
|
|
||||||
if (result) {
|
|
||||||
if (result.status === 'error' && options.args.doNotThrowError === false) {
|
|
||||||
throw new Error(result.remark)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
cert,
|
|
||||||
context,
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async runCertd (certd) {
|
|
||||||
logger.info(`证书任务 ${JSON.stringify(certd.options.cert.domains)} 开始`)
|
|
||||||
const cert = await certd.certApply()
|
|
||||||
logger.info(`证书任务 ${JSON.stringify(certd.options.cert.domains)} 完成`)
|
|
||||||
return cert
|
|
||||||
}
|
|
||||||
|
|
||||||
async runDeploys ({ options, cert, context, trace }) {
|
|
||||||
if (cert == null) {
|
|
||||||
const certd = new Certd(options)
|
|
||||||
cert = await certd.readCurrentCert()
|
|
||||||
}
|
|
||||||
logger.info('部署任务开始')
|
|
||||||
for (const deploy of options.deploy) {
|
|
||||||
const deployName = deploy.deployName
|
|
||||||
logger.info(`------------【${deployName}】-----------`)
|
|
||||||
|
|
||||||
const deployTrace = trace.getInstance({ type: 'deploy', deployName })
|
|
||||||
if (deploy.disabled === true) {
|
|
||||||
logger.info('此流程已被禁用,跳过')
|
|
||||||
logger.info('')
|
|
||||||
deployTrace.set({ value: { current: 'skip', status: 'disabled', remark: '流程禁用' } })
|
|
||||||
deployTrace.set({ tasks: null })
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
for (const task of deploy.tasks) {
|
|
||||||
if (context[deployName] == null) {
|
|
||||||
context[deployName] = {}
|
|
||||||
}
|
|
||||||
const taskContext = context[deployName]
|
|
||||||
// 开始执行任务列表
|
|
||||||
await this.runTask({ options, cert, task, context: taskContext, deploy, trace })
|
|
||||||
}
|
|
||||||
|
|
||||||
deployTrace.set({ value: { status: 'success', remark: '执行成功' } })
|
|
||||||
trace.set({ type: 'result', value: { status: 'success', remark: '执行成功' } })
|
|
||||||
} catch (e) {
|
|
||||||
deployTrace.set({ value: { status: 'error', remark: '执行失败:' + e.message } })
|
|
||||||
trace.set({ type: 'result', value: { status: 'error', remark: deployName + '执行失败:' + e.message } })
|
|
||||||
logger.error('流程执行失败', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async runTask ({ options, task, cert, context, deploy, trace }) {
|
|
||||||
const taskType = task.type
|
|
||||||
const Plugin = pluginRegistry.get(taskType)
|
|
||||||
const deployName = deploy.deployName
|
|
||||||
const taskName = task.taskName
|
|
||||||
if (Plugin == null) {
|
|
||||||
throw new Error(`插件:${taskType}还未安装`)
|
|
||||||
}
|
|
||||||
|
|
||||||
let instance = Plugin
|
|
||||||
if (Plugin instanceof Function) {
|
|
||||||
instance = new Plugin({ accessProviders: options.accessProviders })
|
|
||||||
}
|
|
||||||
const taskTrace = trace.getInstance({ type: 'deploy', deployName, taskName })
|
|
||||||
const traceStatus = taskTrace.get({})
|
|
||||||
if (traceStatus && traceStatus.status === 'success' && !options.args.forceRedeploy) {
|
|
||||||
logger.info(`----【${taskName}】已经执行完成,跳过此任务`)
|
|
||||||
taskTrace.set({ value: { current: 'skip', status: 'success', remark: '已执行成功过,本次跳过' } })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.info(`----【${taskName}】开始执行`)
|
|
||||||
try {
|
|
||||||
// 执行任务
|
|
||||||
await instance.execute({ cert, props: task.props, context })
|
|
||||||
taskTrace.set({ value: { current: 'success', status: 'success', remark: '执行成功', time: dayjs().format() } })
|
|
||||||
} catch (e) {
|
|
||||||
taskTrace.set({ value: { current: 'error', status: 'error', remark: e.message, time: dayjs().format() } })
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
logger.info(`----任务【${taskName}】执行完成`)
|
|
||||||
logger.info('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { util } from '@certd/api'
|
|
||||||
import _ from 'lodash-es'
|
|
||||||
const logger = util.logger
|
|
||||||
export class Trace {
|
|
||||||
constructor (context) {
|
|
||||||
this.context = context
|
|
||||||
}
|
|
||||||
|
|
||||||
getInstance ({ type, deployName, taskName }) {
|
|
||||||
return {
|
|
||||||
get: ({ prop }) => {
|
|
||||||
return this.get({ type, deployName, taskName, prop })
|
|
||||||
},
|
|
||||||
set: ({ prop, value }) => {
|
|
||||||
this.set({ type, deployName, taskName, prop, value })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set ({ type, deployName, taskName, prop, value }) {
|
|
||||||
const key = this.buildTraceKey({ type, deployName, taskName, prop })
|
|
||||||
const oldValue = _.get(this.context, key) || {}
|
|
||||||
_.merge(oldValue, value)
|
|
||||||
_.set(this.context, key, oldValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
get ({ type, deployName, taskName, prop }) {
|
|
||||||
return _.get(this.context, this.buildTraceKey({ type, deployName, taskName, prop }))
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTraceKey ({ type = 'default', deployName, taskName, prop }) {
|
|
||||||
let key = '__trace__.' + type
|
|
||||||
if (deployName) {
|
|
||||||
key += '.'
|
|
||||||
key += deployName.replace(/\./g, '_')
|
|
||||||
}
|
|
||||||
if (taskName) {
|
|
||||||
key += '.tasks.'
|
|
||||||
key += taskName.replace(/\./g, '_')
|
|
||||||
}
|
|
||||||
if (prop) {
|
|
||||||
key += '.' + prop
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
getStringLength (str) {
|
|
||||||
const enLength = str.replace(/[\u0391-\uFFE5]/g, '').length // 先把中文替换成两个字节的英文,再计算长度
|
|
||||||
return Math.floor((str.length - enLength) * 1.5) + enLength
|
|
||||||
}
|
|
||||||
|
|
||||||
print () {
|
|
||||||
const context = this.context
|
|
||||||
logger.info('---------------------------任务结果总览--------------------------')
|
|
||||||
if (context.certIsNew) {
|
|
||||||
this.printTraceLine({ current: 'success', remark: '证书更新成功' }, '更新证书')
|
|
||||||
} else {
|
|
||||||
this.printTraceLine({ current: 'skip', remark: '还未到过期时间,跳过' }, '更新证书')
|
|
||||||
}
|
|
||||||
const trace = this.get({ type: 'deploy' })
|
|
||||||
// logger.info('trace', trace)
|
|
||||||
for (const deployName in trace) {
|
|
||||||
if (trace[deployName] == null) {
|
|
||||||
trace[deployName] = {}
|
|
||||||
}
|
|
||||||
const traceStatus = this.printTraceLine(trace[deployName], deployName)
|
|
||||||
|
|
||||||
const tasks = traceStatus.tasks
|
|
||||||
if (tasks) {
|
|
||||||
for (const taskName in tasks) {
|
|
||||||
if (tasks[taskName] == null) {
|
|
||||||
tasks[taskName] = {}
|
|
||||||
}
|
|
||||||
this.printTraceLine(tasks[taskName], taskName, ' └')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const result = this.get({ type: 'result' })
|
|
||||||
if (result) {
|
|
||||||
this.printTraceLine(result, 'result', '')
|
|
||||||
}
|
|
||||||
const mainContext = {}
|
|
||||||
_.merge(mainContext, context)
|
|
||||||
delete mainContext.__trace__
|
|
||||||
logger.info('【context】', JSON.stringify(mainContext))
|
|
||||||
}
|
|
||||||
|
|
||||||
printTraceLine (traceStatus, name, prefix = '') {
|
|
||||||
const length = this.getStringLength(name)
|
|
||||||
const endPad = _.repeat('-', 45 - prefix.length - length) + '\t'
|
|
||||||
const status = traceStatus.current || traceStatus.status || ''
|
|
||||||
const remark = traceStatus.remark || ''
|
|
||||||
logger.info(`${prefix}【${name}】${endPad}[${status}] \t${remark}`)
|
|
||||||
return traceStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"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
3104
packages/plugins/plugin-aliyun/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@certd/plugin-aliyun",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "src/index.js",
|
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
|
||||||
"@alicloud/pop-core": "^1.7.10",
|
|
||||||
"@certd/api": "^0.2.0",
|
|
||||||
"dayjs": "^1.9.7",
|
|
||||||
"lodash-es": "^4.17.20"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@certd/certd": "^0.2.0",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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: {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { AbstractPlugin } from '@certd/api'
|
|
||||||
|
|
||||||
export class AbstractAliyunPlugin extends AbstractPlugin {
|
|
||||||
checkRet (ret) {
|
|
||||||
if (ret.code != null) {
|
|
||||||
throw new Error('执行失败:', ret.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"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
3166
packages/plugins/plugin-host/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@certd/plugin-host",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "src/index.js",
|
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
|
||||||
"@certd/api": "^0.2.0",
|
|
||||||
"dayjs": "^1.9.7",
|
|
||||||
"lodash-es": "^4.17.20",
|
|
||||||
"ssh2": "^0.8.9"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@certd/certd": "^0.2.0",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
export class SSHAccessProvider {
|
|
||||||
static define () {
|
|
||||||
return {
|
|
||||||
name: 'ssh',
|
|
||||||
label: '主机',
|
|
||||||
desc: '',
|
|
||||||
input: {
|
|
||||||
host: { required: true },
|
|
||||||
port: {
|
|
||||||
label: '端口',
|
|
||||||
type: Number,
|
|
||||||
default: '22',
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
username: {
|
|
||||||
default: 'root',
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
password: { desc: '登录密码' },
|
|
||||||
privateKey: {
|
|
||||||
desc: '密钥,密码或此项必填一项'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { AbstractPlugin } from '@certd/api'
|
|
||||||
|
|
||||||
export class AbstractHostPlugin extends AbstractPlugin {
|
|
||||||
checkRet (ret) {
|
|
||||||
if (ret.code != null) {
|
|
||||||
throw new Error('执行失败:', ret.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
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 }) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import ssh2 from 'ssh2'
|
|
||||||
import path from 'path'
|
|
||||||
import { util } from '@certd/api'
|
|
||||||
import _ from 'lodash-es'
|
|
||||||
const logger = util.logger
|
|
||||||
export class SshClient {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param connectConf
|
|
||||||
{
|
|
||||||
host: '192.168.100.100',
|
|
||||||
port: 22,
|
|
||||||
username: 'frylock',
|
|
||||||
password: 'nodejsrules'
|
|
||||||
}
|
|
||||||
* @param transports
|
|
||||||
*/
|
|
||||||
uploadFiles ({ connectConf, transports, sudo = false }) {
|
|
||||||
const conn = new ssh2.Client()
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
conn.on('ready', () => {
|
|
||||||
logger.info('连接服务器成功')
|
|
||||||
conn.sftp(async (err, sftp) => {
|
|
||||||
if (err) {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const transport of transports) {
|
|
||||||
logger.info('上传文件:', JSON.stringify(transport))
|
|
||||||
sudo = sudo ? 'sudo' : ''
|
|
||||||
await this.exec({ connectConf, script: `${sudo} mkdir -p ${path.dirname(transport.remotePath)} ` })
|
|
||||||
await this.fastPut({ sftp, ...transport })
|
|
||||||
}
|
|
||||||
resolve()
|
|
||||||
} catch (e) {
|
|
||||||
reject(e)
|
|
||||||
} finally {
|
|
||||||
conn.end()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).connect(connectConf)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
exec ({ connectConf, script }) {
|
|
||||||
if (_.isArray(script)) {
|
|
||||||
script = script.join('\n')
|
|
||||||
}
|
|
||||||
console.log('执行命令:', script)
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.connect({
|
|
||||||
connectConf,
|
|
||||||
onReady: (conn) => {
|
|
||||||
conn.exec(script, (err, stream) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let data = null
|
|
||||||
stream.on('close', (code, signal) => {
|
|
||||||
console.log(`[${connectConf.host}][close]:code:${code}`)
|
|
||||||
data = data ? data.toString() : null
|
|
||||||
if (code === 0) {
|
|
||||||
resolve(data)
|
|
||||||
} else {
|
|
||||||
reject(new Error(data))
|
|
||||||
}
|
|
||||||
conn.end()
|
|
||||||
}).on('data', (ret) => {
|
|
||||||
console.log(`[${connectConf.host}][info]: ` + ret)
|
|
||||||
data = ret
|
|
||||||
}).stderr.on('data', (err) => {
|
|
||||||
console.log(`[${connectConf.host}][error]: ` + err)
|
|
||||||
data = err
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
shell ({ connectConf, script }) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.connect({
|
|
||||||
connectConf,
|
|
||||||
onReady: (conn) => {
|
|
||||||
conn.shell((err, stream) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const output = []
|
|
||||||
stream.on('close', () => {
|
|
||||||
logger.info('Stream :: close')
|
|
||||||
conn.end()
|
|
||||||
resolve(output)
|
|
||||||
}).on('data', (data) => {
|
|
||||||
logger.info('' + data)
|
|
||||||
output.push('' + data)
|
|
||||||
})
|
|
||||||
stream.end(script + '\nexit\n')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
connect ({ connectConf, onReady }) {
|
|
||||||
const conn = new ssh2.Client()
|
|
||||||
conn.on('ready', () => {
|
|
||||||
console.log('Client :: ready')
|
|
||||||
onReady(conn)
|
|
||||||
}).connect(connectConf)
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
fastPut ({ sftp, localPath, remotePath }) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
sftp.fastPut(localPath, remotePath, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { AbstractHostPlugin } from '../abstract-host.js'
|
|
||||||
import { SshClient } from '../ssh.js'
|
|
||||||
export class UploadCertToHost extends AbstractHostPlugin {
|
|
||||||
/**
|
|
||||||
* 插件定义
|
|
||||||
* 名称
|
|
||||||
* 入参
|
|
||||||
* 出参
|
|
||||||
*/
|
|
||||||
static define () {
|
|
||||||
return {
|
|
||||||
name: 'uploadCertToHost',
|
|
||||||
label: '上传证书到主机',
|
|
||||||
input: {
|
|
||||||
crtPath: {
|
|
||||||
label: '证书保存路径'
|
|
||||||
},
|
|
||||||
keyPath: {
|
|
||||||
label: '私钥保存路径'
|
|
||||||
},
|
|
||||||
accessProvider: {
|
|
||||||
label: '主机登录配置',
|
|
||||||
type: [String, Object],
|
|
||||||
desc: 'access授权',
|
|
||||||
component: {
|
|
||||||
name: 'access-provider-selector',
|
|
||||||
filter: 'ssh'
|
|
||||||
},
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
sudo: {
|
|
||||||
label: '是否sudo'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
hostCrtPath: {
|
|
||||||
type: String,
|
|
||||||
desc: '上传成功后的证书路径'
|
|
||||||
},
|
|
||||||
hostKeyPath: {
|
|
||||||
type: String,
|
|
||||||
desc: '上传成功后的私钥路径'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute ({ cert, props, context }) {
|
|
||||||
const { crtPath, keyPath, accessProvider } = props
|
|
||||||
const connectConf = this.getAccessProvider(accessProvider)
|
|
||||||
const sshClient = new SshClient()
|
|
||||||
await sshClient.uploadFiles({
|
|
||||||
connectConf,
|
|
||||||
transports: [
|
|
||||||
{
|
|
||||||
localPath: cert.crtPath,
|
|
||||||
remotePath: crtPath
|
|
||||||
},
|
|
||||||
{
|
|
||||||
localPath: cert.keyPath,
|
|
||||||
remotePath: keyPath
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
this.logger.info('证书上传成功:crtPath=', crtPath, ',keyPath=', keyPath)
|
|
||||||
|
|
||||||
context.hostCrtPath = crtPath
|
|
||||||
context.hostKeyPath = keyPath
|
|
||||||
return {
|
|
||||||
hostCrtPath: crtPath,
|
|
||||||
hostKeyPath: keyPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param cert
|
|
||||||
* @param props
|
|
||||||
* @param context
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async rollback ({ cert, props, context }) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import pkg from 'chai'
|
|
||||||
import { HostShellExecute } from '../../src/plugins/host-shell-execute/index.js'
|
|
||||||
import { Certd } from '@certd/certd'
|
|
||||||
import { createOptions } from '../../../../../test/options.js'
|
|
||||||
const { expect } = pkg
|
|
||||||
describe('HostShellExecute', function () {
|
|
||||||
it('#execute', async function () {
|
|
||||||
this.timeout(10000)
|
|
||||||
const options = createOptions()
|
|
||||||
options.args = { test: false }
|
|
||||||
options.cert.email = 'xiaojunnuo@qq.com'
|
|
||||||
options.cert.domains = ['*.docmirror.cn']
|
|
||||||
const plugin = new HostShellExecute(options)
|
|
||||||
const certd = new Certd(options)
|
|
||||||
const cert = await certd.readCurrentCert()
|
|
||||||
const context = {}
|
|
||||||
const uploadOpts = {
|
|
||||||
cert,
|
|
||||||
props: { script: ['ls ', 'ls '], accessProvider: 'aliyun-ssh' },
|
|
||||||
context
|
|
||||||
}
|
|
||||||
const ret = await plugin.doExecute(uploadOpts)
|
|
||||||
expect(ret).ok
|
|
||||||
console.log('-----' + JSON.stringify(ret))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('#execute-hk-restart-docker', async function () {
|
|
||||||
this.timeout(10000)
|
|
||||||
const options = createOptions()
|
|
||||||
const plugin = new HostShellExecute(options)
|
|
||||||
const uploadOpts = {
|
|
||||||
props: { script: ['cd /home/ubuntu/deloy/nginx-proxy\nsudo docker-compose build\nsudo docker-compose up -d\n'], accessProvider: 'aliyun-ssh-hk' },
|
|
||||||
context: {}
|
|
||||||
}
|
|
||||||
const ret = await plugin.doExecute(uploadOpts)
|
|
||||||
expect(ret).ok
|
|
||||||
console.log('-----' + JSON.stringify(ret))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('#execute-publicKey-login', async function () {
|
|
||||||
this.timeout(10000)
|
|
||||||
const options = createOptions()
|
|
||||||
const plugin = new HostShellExecute(options)
|
|
||||||
const shellOpts = {
|
|
||||||
props: { script: ['ls'], accessProvider: 'tencent-ssh-base01' },
|
|
||||||
context: {}
|
|
||||||
}
|
|
||||||
const ret = await plugin.doExecute(shellOpts)
|
|
||||||
expect(ret).ok
|
|
||||||
console.log('-----' + JSON.stringify(ret))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import pkg from 'chai'
|
|
||||||
import { UploadCertToHost } from '../../src/plugins/upload-to-host/index.js'
|
|
||||||
import { Certd } from '@certd/certd'
|
|
||||||
import { createOptions } from '../../../../../test/options.js'
|
|
||||||
const { expect } = pkg
|
|
||||||
describe('PluginUploadToHost', function () {
|
|
||||||
it('#execute', async function () {
|
|
||||||
this.timeout(10000)
|
|
||||||
const options = createOptions()
|
|
||||||
options.args = { test: false }
|
|
||||||
options.cert.email = 'xiaojunnuo@qq.com'
|
|
||||||
options.cert.domains = ['*.docmirror.cn']
|
|
||||||
const plugin = new UploadCertToHost(options)
|
|
||||||
const certd = new Certd(options)
|
|
||||||
const cert = await certd.readCurrentCert()
|
|
||||||
const context = {}
|
|
||||||
const uploadOpts = {
|
|
||||||
cert,
|
|
||||||
props: { crtPath: '/root/certd/test/test.crt', keyPath: '/root/certd/test/test.key', accessProvider: 'aliyun-ssh' },
|
|
||||||
context
|
|
||||||
}
|
|
||||||
await plugin.doExecute(uploadOpts)
|
|
||||||
console.log('context:', context)
|
|
||||||
|
|
||||||
await plugin.doRollback(uploadOpts)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('#execute-to-ubantu', async function () {
|
|
||||||
this.timeout(10000)
|
|
||||||
const options = createOptions()
|
|
||||||
options.args = { test: false }
|
|
||||||
options.cert.email = 'xiaojunnuo@qq.com'
|
|
||||||
options.cert.domains = ['*.docmirror.cn']
|
|
||||||
const plugin = new UploadCertToHost(options)
|
|
||||||
const certd = new Certd(options)
|
|
||||||
const cert = await certd.readCurrentCert()
|
|
||||||
const context = {}
|
|
||||||
const uploadOpts = {
|
|
||||||
cert,
|
|
||||||
props: { crtPath: '/home/ubuntu/deloy/nginx-proxy/ssl/test.crt', keyPath: '/home/ubuntu/deloy/nginx-proxy/ssl/test.key', accessProvider: 'aliyun-ssh-hk' },
|
|
||||||
context
|
|
||||||
}
|
|
||||||
await plugin.doExecute(uploadOpts)
|
|
||||||
console.log('context:', context)
|
|
||||||
|
|
||||||
await plugin.doRollback(uploadOpts)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"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
3752
packages/plugins/plugin-tencent/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@certd/plugin-tencent",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "src/index.js",
|
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
|
||||||
"@certd/api": "^0.2.0",
|
|
||||||
"dayjs": "^1.9.7",
|
|
||||||
"kubernetes-client": "^9.0.0",
|
|
||||||
"lodash-es": "^4.17.20",
|
|
||||||
"tencentcloud-sdk-nodejs": "^4.0.44"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@certd/certd": "^0.2.0",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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: '该项必填' }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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: '该项必填' }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { AbstractDnsProvider, util } from '@certd/api'
|
|
||||||
import _ from 'lodash-es'
|
|
||||||
const request = util.request
|
|
||||||
export class DnspodDnsProvider extends AbstractDnsProvider {
|
|
||||||
static define () {
|
|
||||||
return {
|
|
||||||
name: 'dnspod',
|
|
||||||
label: 'dnspod(腾讯云)',
|
|
||||||
desc: '腾讯云的域名解析接口已迁移到dnspod',
|
|
||||||
input: {
|
|
||||||
accessProvider: {
|
|
||||||
label: '授权',
|
|
||||||
type: [String, Object],
|
|
||||||
desc: '需要dnspod类型的授权',
|
|
||||||
component: {
|
|
||||||
name: 'access-provider-selector',
|
|
||||||
filter: 'dnspod'
|
|
||||||
},
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor (args) {
|
|
||||||
super(args)
|
|
||||||
const { props } = args
|
|
||||||
const accessProvider = this.getAccessProvider(props.accessProvider)
|
|
||||||
this.loginToken = accessProvider.id + ',' + accessProvider.token
|
|
||||||
}
|
|
||||||
|
|
||||||
async doRequest (options, successCodes = []) {
|
|
||||||
const config = {
|
|
||||||
method: 'post',
|
|
||||||
formData: {
|
|
||||||
login_token: this.loginToken,
|
|
||||||
format: 'json',
|
|
||||||
lang: 'cn',
|
|
||||||
error_on_empty: 'no'
|
|
||||||
},
|
|
||||||
timeout: 5000
|
|
||||||
}
|
|
||||||
_.merge(config, options)
|
|
||||||
|
|
||||||
const ret = await request(config)
|
|
||||||
if (!ret || !ret.status) {
|
|
||||||
const code = ret.status.code
|
|
||||||
if (code !== '1' || !successCodes.includes(code)) {
|
|
||||||
throw new Error('请求失败:' + ret.status.message + ',api=' + config.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDomainList () {
|
|
||||||
const ret = await this.doRequest({
|
|
||||||
url: 'https://dnsapi.cn/Domain.List'
|
|
||||||
})
|
|
||||||
this.logger.debug('dnspod 域名列表:', ret.domains)
|
|
||||||
return ret.domains
|
|
||||||
}
|
|
||||||
|
|
||||||
async createRecord ({ fullRecord, type, value }) {
|
|
||||||
this.logger.info('添加域名解析:', fullRecord, value)
|
|
||||||
const domainItem = await this.matchDomain(fullRecord, 'name')
|
|
||||||
const domain = domainItem.name
|
|
||||||
const rr = fullRecord.replace('.' + domain, '')
|
|
||||||
|
|
||||||
const ret = await this.doRequest({
|
|
||||||
url: 'https://dnsapi.cn/Record.Create',
|
|
||||||
formData: {
|
|
||||||
domain,
|
|
||||||
sub_domain: rr,
|
|
||||||
record_type: type,
|
|
||||||
record_line: '默认',
|
|
||||||
value: value,
|
|
||||||
mx: 1
|
|
||||||
}
|
|
||||||
}, ['104'])// 104错误码为记录已存在,无需再次添加
|
|
||||||
this.logger.info('添加域名解析成功:', fullRecord, value, JSON.stringify(ret.record))
|
|
||||||
return ret.record
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeRecord ({ fullRecord, type, value, record }) {
|
|
||||||
const domain = await this.matchDomain(fullRecord, 'name')
|
|
||||||
|
|
||||||
const ret = await this.doRequest({
|
|
||||||
url: 'https://dnsapi.cn/Record.Remove',
|
|
||||||
formData: {
|
|
||||||
domain,
|
|
||||||
record_id: record.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.logger.info('删除域名解析成功:', fullRecord, value)
|
|
||||||
return ret.RecordId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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, '_')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
import { AbstractTencentPlugin } from '../abstract-tencent.js'
|
|
||||||
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
|
||||||
import { K8sClient } from '../../utils/util.k8s.client.js'
|
|
||||||
export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin {
|
|
||||||
/**
|
|
||||||
* 插件定义
|
|
||||||
* 名称
|
|
||||||
* 入参
|
|
||||||
* 出参
|
|
||||||
*/
|
|
||||||
static define () {
|
|
||||||
return {
|
|
||||||
name: 'deployCertToTencentTKEIngress',
|
|
||||||
label: '部署到腾讯云TKE-ingress',
|
|
||||||
desc: '需要【上传到腾讯云】作为前置任务',
|
|
||||||
input: {
|
|
||||||
region: {
|
|
||||||
label: '大区',
|
|
||||||
default: 'ap-guangzhou',
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
clusterId: {
|
|
||||||
label: '集群ID',
|
|
||||||
required: true,
|
|
||||||
desc: '例如:cls-6lbj1vee',
|
|
||||||
request: true
|
|
||||||
},
|
|
||||||
namespace: {
|
|
||||||
label: '集群namespace',
|
|
||||||
default: 'default',
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
secreteName: {
|
|
||||||
type: [String, Array],
|
|
||||||
label: '证书的secret名称',
|
|
||||||
desc: '支持多个(传入数组)',
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
ingressName: {
|
|
||||||
type: [String, Array],
|
|
||||||
label: 'ingress名称',
|
|
||||||
desc: '支持多个(传入数组)'
|
|
||||||
},
|
|
||||||
ingressClass: {
|
|
||||||
type: String,
|
|
||||||
label: 'ingress类型',
|
|
||||||
desc: '可选 qcloud / nginx'
|
|
||||||
},
|
|
||||||
clusterIp: {
|
|
||||||
type: String,
|
|
||||||
label: '集群内网ip',
|
|
||||||
desc: '如果开启了外网的话,无需设置'
|
|
||||||
},
|
|
||||||
clusterDomain: {
|
|
||||||
type: String,
|
|
||||||
label: '集群域名',
|
|
||||||
desc: '可不填,默认为:[clusterId].ccs.tencent-cloud.com'
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* AccessProvider的key,或者一个包含access的具体的对象
|
|
||||||
*/
|
|
||||||
accessProvider: {
|
|
||||||
label: 'Access授权',
|
|
||||||
type: [String, Object],
|
|
||||||
desc: 'access授权',
|
|
||||||
component: {
|
|
||||||
name: 'access-provider-selector',
|
|
||||||
filter: 'tencent'
|
|
||||||
},
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute ({ cert, props, context }) {
|
|
||||||
const accessProvider = this.getAccessProvider(props.accessProvider)
|
|
||||||
const tkeClient = this.getTkeClient(accessProvider, props.region)
|
|
||||||
const kubeConfigStr = await this.getTkeKubeConfig(tkeClient, props.clusterId)
|
|
||||||
|
|
||||||
this.logger.info('kubeconfig已成功获取')
|
|
||||||
const k8sClient = new K8sClient(kubeConfigStr)
|
|
||||||
if (props.clusterIp != null) {
|
|
||||||
let clusterDomain = props.clusterDomain
|
|
||||||
if (!clusterDomain) {
|
|
||||||
clusterDomain = `${props.clusterId}.ccs.tencent-cloud.com`
|
|
||||||
}
|
|
||||||
// 修改内网解析ip地址
|
|
||||||
k8sClient.setLookup({ [clusterDomain]: { ip: props.clusterIp } })
|
|
||||||
}
|
|
||||||
const ingressType = props.ingressClass || 'qcloud'
|
|
||||||
if (ingressType === 'qcloud') {
|
|
||||||
await this.patchQcloudCertSecret({ k8sClient, props, context })
|
|
||||||
} else {
|
|
||||||
await this.patchNginxCertSecret({ cert, k8sClient, props, context })
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.sleep(2000) // 停留2秒,等待secret部署完成
|
|
||||||
await this.restartIngress({ k8sClient, props })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
getTkeClient (accessProvider, region = 'ap-guangzhou') {
|
|
||||||
const TkeClient = tencentcloud.tke.v20180525.Client
|
|
||||||
const clientConfig = {
|
|
||||||
credential: {
|
|
||||||
secretId: accessProvider.secretId,
|
|
||||||
secretKey: accessProvider.secretKey
|
|
||||||
},
|
|
||||||
region,
|
|
||||||
profile: {
|
|
||||||
httpProfile: {
|
|
||||||
endpoint: 'tke.tencentcloudapi.com'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TkeClient(clientConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTkeKubeConfig (client, clusterId) {
|
|
||||||
// Depends on tencentcloud-sdk-nodejs version 4.0.3 or higher
|
|
||||||
const params = {
|
|
||||||
ClusterId: clusterId
|
|
||||||
}
|
|
||||||
const ret = await client.DescribeClusterKubeconfig(params)
|
|
||||||
this.checkRet(ret)
|
|
||||||
this.logger.info('注意:后续操作需要在【集群->基本信息】中开启外网或内网访问,https://console.cloud.tencent.com/tke2/cluster')
|
|
||||||
return ret.Kubeconfig
|
|
||||||
}
|
|
||||||
|
|
||||||
async patchQcloudCertSecret ({ k8sClient, props, context }) {
|
|
||||||
const { tencentCertId } = context
|
|
||||||
if (tencentCertId == null) {
|
|
||||||
throw new Error('请先将【上传证书到腾讯云】作为前置任务')
|
|
||||||
}
|
|
||||||
this.logger.info('腾讯云证书ID:', tencentCertId)
|
|
||||||
const certIdBase64 = Buffer.from(tencentCertId).toString('base64')
|
|
||||||
|
|
||||||
const { namespace, secretName } = props
|
|
||||||
|
|
||||||
const body = {
|
|
||||||
data: {
|
|
||||||
qcloud_cert_id: certIdBase64
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
labels: {
|
|
||||||
certd: this.appendTimeSuffix('certd')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let secretNames = secretName
|
|
||||||
if (typeof secretName === 'string') {
|
|
||||||
secretNames = [secretName]
|
|
||||||
}
|
|
||||||
for (const secret of secretNames) {
|
|
||||||
await k8sClient.patchSecret({ namespace, secretName: secret, body })
|
|
||||||
this.logger.info(`CertSecret已更新:${secret}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async patchNginxCertSecret ({ cert, k8sClient, props, context }) {
|
|
||||||
const crt = cert.crt
|
|
||||||
const key = cert.key
|
|
||||||
const crtBase64 = Buffer.from(crt).toString('base64')
|
|
||||||
const keyBase64 = Buffer.from(key).toString('base64')
|
|
||||||
|
|
||||||
const { namespace, secretName } = props
|
|
||||||
|
|
||||||
const body = {
|
|
||||||
data: {
|
|
||||||
'tls.crt': crtBase64,
|
|
||||||
'tls.key': keyBase64
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
labels: {
|
|
||||||
certd: this.appendTimeSuffix('certd')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let secretNames = secretName
|
|
||||||
if (typeof secretName === 'string') {
|
|
||||||
secretNames = [secretName]
|
|
||||||
}
|
|
||||||
for (const secret of secretNames) {
|
|
||||||
await k8sClient.patchSecret({ namespace, secretName: secret, body })
|
|
||||||
this.logger.info(`CertSecret已更新:${secret}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async restartIngress ({ k8sClient, props }) {
|
|
||||||
const { namespace, ingressName } = props
|
|
||||||
|
|
||||||
const body = {
|
|
||||||
metadata: {
|
|
||||||
labels: {
|
|
||||||
certd: this.appendTimeSuffix('certd')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let ingressNames = ingressName
|
|
||||||
if (typeof ingressName === 'string') {
|
|
||||||
ingressNames = [ingressName]
|
|
||||||
}
|
|
||||||
for (const ingress of ingressNames) {
|
|
||||||
await k8sClient.patchIngress({ namespace, ingressName: ingress, body })
|
|
||||||
this.logger.info(`ingress已重启:${ingress}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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 })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import pkg from 'chai'
|
|
||||||
import { DeployCertToTencentTKEIngress } from '../../src/plugins/deploy-to-tke-ingress/index.js'
|
|
||||||
import { Certd } from '@certd/certd'
|
|
||||||
import { createOptions } from '../../../../../test/options.js'
|
|
||||||
import { K8sClient } from '../../src/utils/util.k8s.client.js'
|
|
||||||
|
|
||||||
const { expect } = pkg
|
|
||||||
|
|
||||||
async function getOptions () {
|
|
||||||
const options = createOptions()
|
|
||||||
options.args.test = false
|
|
||||||
options.cert.email = 'xiaojunnuo@qq.com'
|
|
||||||
options.cert.domains = ['*.docmirror.cn']
|
|
||||||
const certd = new Certd(options)
|
|
||||||
const cert = await certd.readCurrentCert()
|
|
||||||
const context = {}
|
|
||||||
const deployOpts = {
|
|
||||||
accessProviders: options.accessProviders,
|
|
||||||
cert,
|
|
||||||
props: {
|
|
||||||
accessProvider: 'tencent-yonsz',
|
|
||||||
region: 'ap-guangzhou',
|
|
||||||
clusterId: 'cls-6lbj1vee'
|
|
||||||
},
|
|
||||||
context
|
|
||||||
}
|
|
||||||
return { options, deployOpts }
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('DeployCertToTencentTKEIngressNginx', function () {
|
|
||||||
it('#getTKESecrets', async function () {
|
|
||||||
this.timeout(50000)
|
|
||||||
const { options, deployOpts } = await getOptions()
|
|
||||||
const plugin = new DeployCertToTencentTKEIngress(options)
|
|
||||||
const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
|
|
||||||
const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props.clusterId)
|
|
||||||
|
|
||||||
const k8sClient = new K8sClient(kubeConfig)
|
|
||||||
k8sClient.setLookup({
|
|
||||||
'cls-6lbj1vee.ccs.tencent-cloud.com': { ip: '13.123.123.123' }
|
|
||||||
})
|
|
||||||
const secrets = await k8sClient.getSecret({ namespace: 'stress' })
|
|
||||||
|
|
||||||
console.log('secrets:', secrets)
|
|
||||||
})
|
|
||||||
it('#execute', async function () {
|
|
||||||
this.timeout(5000)
|
|
||||||
|
|
||||||
const { options, deployOpts } = await getOptions()
|
|
||||||
deployOpts.props.ingressName = 'stress-ingress-nginx'
|
|
||||||
deployOpts.props.ingressClass = 'nginx'
|
|
||||||
deployOpts.props.secretName = 'stress-all'
|
|
||||||
deployOpts.props.namespace = 'stress'
|
|
||||||
const plugin = new DeployCertToTencentTKEIngress(options)
|
|
||||||
|
|
||||||
const ret = await plugin.doExecute(deployOpts)
|
|
||||||
console.log('sucess', ret)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
16
packages/ui/certd-server/.dockerignore
Normal file
16
packages/ui/certd-server/.dockerignore
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
logs/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
coverage/
|
||||||
|
!dist/
|
||||||
|
.idea/
|
||||||
|
run/
|
||||||
|
.DS_Store
|
||||||
|
*.sw*
|
||||||
|
*.un~
|
||||||
|
.tsbuildinfo
|
||||||
|
.tsbuildinfo.*
|
||||||
|
/data/db.sqlite
|
||||||
11
packages/ui/certd-server/.editorconfig
Normal file
11
packages/ui/certd-server/.editorconfig
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# 🎨 editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
packages/ui/certd-server/.eslintrc.json
Normal file
7
packages/ui/certd-server/.eslintrc.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "./node_modules/mwts/",
|
||||||
|
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user