Compare commits

..

100 Commits

Author SHA1 Message Date
xiaojunnuo
ef1896fdcf v0.3.1 2022-12-28 12:58:13 +08:00
xiaojunnuo
7438cea8d1 refactor: 0.3.1 2022-12-28 12:56:16 +08:00
xiaojunnuo
6950d779e0 perf: ui 升级到antdv3.x 2022-10-18 09:40:00 +08:00
xiaojunnuo
4c31db427a perf: ui 升级到antdv3.x 2022-10-17 22:04:12 +08:00
xiaojunnuo
540981e071 refactor: 1 2022-10-17 20:06:41 +08:00
xiaojunnuo
ed8b9cd5e9 refactor: 1 2022-10-17 17:06:10 +08:00
xiaojunnuo
3dea83ce31 refactor: 1 2022-10-17 17:01:37 +08:00
xiaojunnuo
cfb46a7c2b v0.3.0 2022-10-17 17:00:47 +08:00
xiaojunnuo
dd2f44bfe9 refactor: 1 2022-10-17 17:00:33 +08:00
xiaojunnuo
09d0e79bcd feat: 升级client0.3.0 2022-10-17 16:59:07 +08:00
xiaojunnuo
a9c1ca5c97 refactor: 1 2022-10-17 16:57:54 +08:00
xiaojunnuo
1af6fb3a58 refactor: 1 2022-10-17 16:54:01 +08:00
xiaojunnuo
a38004c1ed upgrade client 2022-09-28 17:11:32 +08:00
xiaojunnuo
5a355b276e upgrade client 2022-09-28 17:11:25 +08:00
xiaojunnuo
a52721bd7e upgrade client 2022-09-28 16:52:26 +08:00
xiaojunnuo
6df399fe02 upgrade client 2022-09-28 16:50:38 +08:00
xiaojunnuo
6d8ede9575 client submodules 2022-09-28 15:43:54 +08:00
xiaojunnuo
7145aa60ca v0.2.2 2021-12-04 18:43:58 +08:00
xiaojunnuo
b1181c0f9a refactor: 1 2021-12-04 18:39:05 +08:00
xiaojunnuo
bace2a7c25 refactor: 1 2021-12-04 18:34:52 +08:00
xiaojunnuo
a70b4373de v0.2.1 2021-12-04 17:11:07 +08:00
xiaojunnuo
b7c12e6d91 perf: 支持阿里云 ack ingress 2021-12-04 16:57:12 +08:00
xiaojunnuo
6a88dd476e refactor: 0.2.0 2021-12-02 17:07:10 +08:00
xiaojunnuo
5fbd774266 v0.2.0 2021-12-02 17:01:24 +08:00
xiaojunnuo
bdec010d2e refactor: 1 2021-12-02 17:00:24 +08:00
xiaojunnuo
05a00b7b78 feat: 私钥升级为PKCS8 2021-12-02 16:47:46 +08:00
xiaojunnuo
eaf23c3034 v0.1.21 2021-11-04 18:02:14 +08:00
xiaojunnuo
276a8b35e5 feat: 支持腾讯云nginx-ingress 2021-11-04 18:00:30 +08:00
xiaojunnuo
466d659f6e fix: 修改ssh privateKey参数名 2021-06-09 17:50:18 +08:00
xiaojunnuo
84e26381b5 v0.1.20 2021-06-02 09:15:30 +08:00
xiaojunnuo
469b5a5f69 fix: fix 任务成功后不需要重新运行 2021-06-02 09:14:10 +08:00
xiaojunnuo
ad77ebd2f9 refactor: 1 2021-03-17 18:06:06 +08:00
xiaojunnuo
b75543c3bc v0.1.19 2021-03-16 19:14:31 +08:00
xiaojunnuo
0677275742 refactor: 1 2021-03-16 18:40:16 +08:00
xiaojunnuo
0c3724e0ad refactor: 1 2021-03-16 18:27:24 +08:00
xiaojunnuo
803083d23c refactor: 1 2021-03-16 18:25:11 +08:00
xiaojunnuo
f4f8067a12 refactor: new client 2021-03-15 19:04:46 +08:00
xiaojunnuo
caa9f084d6 v0.1.18 2021-03-15 11:54:09 +08:00
xiaojunnuo
81407b65d1 refactor: dir with md5 2021-02-26 15:27:34 +08:00
xiaojunnuo
8a24293fd7 refactor: dir with md5 2021-02-26 15:25:04 +08:00
xiaojunnuo
8f1886a585 refactor: dir with suffix 2021-02-26 15:23:53 +08:00
xiaojunnuo
0a64e5fa67 refactor: md5 dir 2021-02-26 15:22:21 +08:00
xiaojunnuo
7a70603971 refactor: doc 2021-02-09 21:23:58 +08:00
xiaojunnuo
0d5e00e744 refactor: doc 2021-02-09 21:22:07 +08:00
xiaojunnuo
91ba1433af refactor: doc 2021-02-09 21:20:41 +08:00
xiaojunnuo
12e56d14f2 refactor: icons 2021-02-09 21:13:19 +08:00
xiaojunnuo
7326119f52 refactor: export 2021-02-09 21:07:19 +08:00
xiaojunnuo
136983cf14 refactor: export 2021-02-09 19:01:15 +08:00
xiaojunnuo
105a1b80ae refactor: export 2021-02-09 18:40:39 +08:00
xiaojunnuo
b8000ca533 refactor: export 2021-02-09 18:40:29 +08:00
xiaojunnuo
c3e374e6e5 refactor: export 2021-02-09 18:05:01 +08:00
xiaojunnuo
a9b6e87249 refactor: 按需加载 2021-02-09 17:19:00 +08:00
xiaojunnuo
61de5422bf refactor: logo 2021-02-09 16:30:59 +08:00
xiaojunnuo
f96697f619 refactor: logo 2021-02-09 15:42:10 +08:00
xiaojunnuo
b4560d6370 refactor: docker 2021-02-09 11:03:11 +08:00
xiaojunnuo
a7bcde8d82 refactor: host 2021-02-09 10:57:08 +08:00
xiaojunnuo
34bb4d54c2 refactor: deploy image 2021-02-08 23:01:07 +08:00
xiaojunnuo
e0116a1a03 refactor: deploy image 2021-02-08 22:37:54 +08:00
xiaojunnuo
12fec7939d refactor: deploy 2021-02-08 21:16:56 +08:00
xiaojunnuo
ff8e02cceb v0.1.17 2021-02-08 18:19:45 +08:00
xiaojunnuo
8122bed97f refactor: host 2021-02-08 18:18:23 +08:00
xiaojunnuo
991c3dbb76 v0.1.16 2021-02-08 15:46:27 +08:00
xiaojunnuo
399c23623d refactor: host 2021-02-08 15:46:03 +08:00
xiaojunnuo
2232f21b48 refactor: transfer 2021-02-08 15:12:35 +08:00
xiaojunnuo
e41c084381 refactor: transfer 2021-02-08 15:00:04 +08:00
xiaojunnuo
520b27e0dc refactor: ui 2021-02-08 14:31:12 +08:00
xiaojunnuo
ace7e0247a v0.1.15 2021-02-08 14:18:14 +08:00
xiaojunnuo
9ae414b1c6 refactor: 重构 2021-02-08 14:00:28 +08:00
xiaojunnuo
cb8c8186f1 refactor: 重构 2021-02-08 13:40:28 +08:00
xiaojunnuo
82f86d9556 refactor: move 2021-02-08 00:21:36 +08:00
xiaojunnuo
cfb1034450 refactor: host 2021-02-07 23:17:44 +08:00
xiaojunnuo
2a07442a85 refactor: ui 2021-02-07 18:32:38 +08:00
xiaojunnuo
68c1eff81d refactor: fix 2021-02-07 13:53:30 +08:00
xiaojunnuo
baec15dfc6 refactor: fix 2021-02-07 13:47:09 +08:00
xiaojunnuo
6eb9817296 refactor: fix 2021-02-07 10:54:55 +08:00
xiaojunnuo
b9d5d33aaa refactor: delete task 2021-02-05 18:20:33 +08:00
xiaojunnuo
560519894c refactor: fix bug 2021-02-05 18:13:24 +08:00
xiaojunnuo
9f434b0968 v0.1.14 2021-02-05 17:19:35 +08:00
xiaojunnuo
07066dde87 refactor: exitCode 2021-02-05 17:19:20 +08:00
xiaojunnuo
074c8f7cd0 v0.1.13 2021-02-05 17:08:11 +08:00
xiaojunnuo
e9df2355f4 refactor: fix bug 2021-02-05 17:07:49 +08:00
xiaojunnuo
45547d6f94 refactor: 0.1.12 2021-02-05 14:45:53 +08:00
xiaojunnuo
4a421d5b14 v0.1.12 2021-02-05 14:32:14 +08:00
xiaojunnuo
7b9825eb40 refactor: 重构优化 2021-02-05 14:31:52 +08:00
xiaojunnuo
5cde165f0b refactor: 重构优化 2021-02-05 14:30:31 +08:00
xiaojunnuo
305824ff1a refactor: ui 2021-02-04 22:07:01 +08:00
xiaojunnuo
86ddb72227 refactor: ui 2021-02-04 21:24:07 +08:00
xiaojunnuo
cca33478e4 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	ui/certd-ui/src/api/util.input.handler.js
2021-02-04 20:32:34 +08:00
xiaojunnuo
a8f41d3c48 refactor: form input 2021-02-04 20:31:04 +08:00
xiaojunnuo
a25a15ca6e refactor: 重构优化 2021-02-04 18:44:16 +08:00
xiaojunnuo
a39dac4dbd refactor: rename ui 2021-02-04 11:17:54 +08:00
xiaojunnuo
eab0c3be60 refactor: form input 2021-01-31 02:09:54 +08:00
xiaojunnuo
b4ee3d0dfc refactor: input render 2021-01-30 00:06:50 +08:00
xiaojunnuo
2f03e18c59 refactor: ui 2021-01-28 01:09:17 +08:00
xiaojunnuo
232cd7215e refactor: ui 2021-01-28 01:07:56 +08:00
xiaojunnuo
86b1e9959b refactor: ui 2021-01-28 01:00:06 +08:00
xiaojunnuo
fd130f86fd refactor: define 2021-01-26 00:58:00 +08:00
xiaojunnuo
2669f509e1 refactor: ui 2021-01-24 00:36:53 +08:00
xiaojunnuo
d3619ad60f refactor: ui prepare 2021-01-21 23:59:06 +08:00
xiaojunnuo
c26417d769 refactor: release 2021-01-18 22:31:45 +08:00
197 changed files with 5401 additions and 29623 deletions

26
.gitignore vendored
View File

@@ -1,11 +1,29 @@
# IntelliJ project files
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/
**/dist
.idea
*.iml
out
gen
node_modules/
/test/*.private.*
/other/node-acme-client/.idea/
/*.log
/other/certd-run
/other/node-acme-client
/packages/ui/*/.idea
/packages/ui/*/node_modules
/packages/*/node_modules
/packages/ui/certd-server/tmp/
/packages/ui/certd-ui/dist/
/other
/dev-sidecar-test
/packages/core/certd/yarn.lock
/packages/test
/test/own

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "packages/base/node-acme-client"]
path = packages/base/node-acme-client
url = https://github.com/certd/node-acme-client.git

101
README.md
View File

@@ -26,88 +26,31 @@ CertD 是一个帮助你全自动申请和部署SSL证书的工具。
## 快速开始
本案例演示如何配置自动申请证书并部署到阿里云CDN然后快要到期前自动更新证书并重新部署
1. 环境准备
安装[nodejs](https://nodejs.org/zh-cn/)
2. 创建任务项目
2. 生成node项目
通过ui生成 https://certd.docmirror.cn/
开始生成证书,先填写域名,支持将多个域名打到一个证书上
![](./doc/step1.png)
配置证书详细信息
![](./doc/step2.png)
配置证书部署流程
![](./doc/step3.png)
配置好之后点击导出按钮导出一个node项目包
4. 运行
将导出的压缩包解压,然后执行如下命令,即可开始申请证书并部署
```
mkdir certd-run # 项目名称可以任意命名
cd certd-run -y
npm install @certd/executor -s --production
```
3. 创建index.js
参数配置分几个部分
args: 运行时参数
accessProviders: 授权提供者提供dns验证与部署任务的授权
cert: 证书申请的配置
deploy 证书部署流程
```js
import { Executor } from '@certd/executor'
const options = {
args: { // 运行时参数
forceDeploy: true,
},
accessProviders: { //授权提供者
aliyun: { // 阿里云accessKey用于dns验证和上传证书到阿里云并部署到cdn
providerType: 'aliyun',
accessKeyId: 'Your accessKeyId',
accessKeySecret: 'Your accessKeySecret'
},
},
cert: { //免费证书申请配置
domains: [ //可以在一张证书上绑定多个域名前提是他们的验证方式要一样目前仅支持dns验证
'*.yourdomain.com',
'*.test.yourdomain.com',
'yourdomain.com'
],
email: 'Your email',
dnsProvider: 'aliyun', //上方accessProviders里面配置的
csrInfo: { //证书csr信息
country: 'CN',
state: 'GuangDong',
locality: 'ShengZhen',
organization: 'Your company Org.',
organizationUnit: 'IT Department',
emailAddress: 'Your email'
}
},
deploy: [ //部署流程配置,数组,可以配置多条流程
{
deployName: '流程1-部署到阿里云CDN',
tasks: [ //流程任务,一个流程下可以包含多个部署任务,并且将按顺序执行
{ //任务1
taskName: '上传到阿里云', //任务名称
type: 'uploadCertToAliyun', //任务插件名称
props: { //任务插件参数
accessProvider: 'aliyun'
}
},
{ // 任务2
taskName: '部署证书到CDN',
type: 'deployCertToAliyunCDN', //任务插件名称
props:{
domainName: 'your cdn domain 全称', //cdn域名全称
certName: 'certd自动部署',//证书名称前缀
accessProvider: 'aliyun'
}
}
]
}
]
}
const executor = new Executor()
await executor.run(options)
```
4. 运行
```
node index.js
npm install
npm run certd
```
5. 执行效果
生成的证书默认会存储在 `${home}/.certd/${email}/certs/${domain}/current`目录下
@@ -129,7 +72,7 @@ node index.js
所以当你临时需要将证书部署到其他地方时,直接追加部署任务,然后再次运行即可
## CI/DI集成与自动续期重新部署
集成前,将以上代码提交到内网git仓库或者私有git仓库由于包含敏感信息不要提交到公开git仓库
集成前,将以上导出的node项目提交到内网git仓库或者私有git仓库由于包含敏感信息不要提交到公开git仓库
### jenkins任务
1. 创建任务

BIN
doc/step1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
doc/step2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
doc/step3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
doc/tasks.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,6 +1,8 @@
{
"packages": [
"packages/*"
"packages/base/*",
"packages/core/*",
"packages/plugins/*"
],
"version": "0.1.11"
"version": "0.3.1"
}

View File

@@ -6,6 +6,8 @@
"lerna": "^3.18.4"
},
"scripts": {
"start": "lerna bootstrap --hoist",
"i-all": "lerna link && lerna exec npm install "
},
"license": "MIT",
"dependencies": {

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

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

View File

@@ -1,8 +1,8 @@
{
"name": "@certd/api",
"version": "0.1.11",
"version": "0.3.1",
"description": "",
"main": "./src/index.js",
"main": "src/index.js",
"type": "module",
"author": "Greper",
"license": "MIT",
@@ -21,5 +21,6 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"mocha": "^8.2.1"
}
},
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -5,6 +5,9 @@ const logger = util.logger
export class AcmeService {
constructor (store) {
this.store = store
acme.setLogger((text) => {
logger.info(text)
})
}
async getAccountConfig (email) {

View File

@@ -1,27 +1,12 @@
import { util, Store } from '@certd/api'
import { util, Store, dnsProviderRegistry } from '@certd/api'
import { AcmeService } from './acme.js'
import { FileStore } from './store/file-store.js'
import { CertStore } from './store/cert-store.js'
import dayjs from 'dayjs'
import forge from 'node-forge'
import DefaultProviders from '@certd/providers'
import _ from 'lodash-es'
const logger = util.logger
const AccessProviderClasses = {}
function install (providerClass) {
AccessProviderClasses[providerClass.name()] = providerClass
}
logger.info('use')
_.forEach(DefaultProviders, item => {
logger.info('use:', item.name())
install(item)
})
export class Certd {
static use (providerClass) {
install(providerClass)
}
constructor (options) {
this.options = options
this.email = options.cert.email
@@ -87,16 +72,14 @@ export class Certd {
}
createDnsProvider (options) {
const accessProviders = options.accessProviders
const providerOptions = accessProviders[options.cert.dnsProvider]
return this.createProviderByType(providerOptions.providerType, providerOptions)
return this.createProviderByType(options.cert.dnsProvider, options.accessProviders)
}
async writeCert (cert) {
const newPath = await this.certStore.writeCert(cert)
return {
realPath: this.certStore.store.getActualKey(newPath),
currentPath: this.certStore.store.getActualKey(this.certStore.currentRootPath)
currentPath: this.certStore.store.getActualKey(this.certStore.currentMarkPath)
}
}
@@ -137,12 +120,12 @@ export class Certd {
}
}
createProviderByType (type, options) {
try {
const Provider = AccessProviderClasses[type]
return new Provider(options)
} catch (e) {
throw new Error('暂不支持此dnsProvider,请先use该provider' + type, e)
createProviderByType (props, accessProviders) {
const { type } = props
const Provider = dnsProviderRegistry.get(type)
if (Provider == null) {
throw new Error('暂不支持此dnsProvider,请先注册该provider' + type)
}
return new Provider({ accessProviders, props })
}
}

View File

@@ -1,5 +1,6 @@
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')
}
@@ -11,9 +12,10 @@ export class CertStore {
this.domain = this.getMainDomain(this.domains)
this.safetyDomain = this.getSafetyDomain(this.domain)
this.domainDir = this.safetyDomain + '-' + md5(this.getDomainStr(this.domains))
// this.domainDir = this.safetyDomain
this.certsRootPath = this.store.buildKey(this.email, 'certs')
this.currentRootPath = this.store.buildKey(this.certsRootPath, this.domainDir, 'current')
this.currentMarkPath = this.store.buildKey(this.certsRootPath, this.domainDir, 'current.json')
}
getMainDomain (domains) {
@@ -62,15 +64,19 @@ export class CertStore {
await this.store.set(priKey, this.formatCert(cert.key.toString()))
await this.store.set(csrKey, cert.csr.toString())
await this.store.link(newDir, this.currentRootPath)
await this.store.set(this.currentMarkPath, JSON.stringify({ latest: newDir }))
return newDir
}
async readCert (dir) {
if (dir == null) {
dir = this.currentRootPath
dir = await this.getCurrentDir()
}
if (dir == null) {
return
}
const crtKey = this.buildKey(dir, this.safetyDomain + '.crt')
const priKey = this.buildKey(dir, this.safetyDomain + '.key')
const csrKey = this.buildKey(dir, this.safetyDomain + '.csr')
@@ -99,13 +105,23 @@ export class CertStore {
return domain.replace(/\*/g, '_')
}
getCurrentFile (file) {
const key = this.buildKey(this.currentRootPath, file)
async getCurrentDir () {
const current = await this.store.get(this.currentMarkPath)
if (current == null) {
return null
}
return JSON.parse(current).latest
}
async getCurrentFile (file) {
const currentDir = await this.getCurrentDir()
const key = this.buildKey(currentDir, file)
return this.store.get(key)
}
setCurrentFile (file, value) {
const key = this.buildKey(this.currentRootPath, file)
async setCurrentFile (file, value) {
const currentDir = await this.getCurrentDir()
const key = this.buildKey(currentDir, file)
return this.store.set(key, value)
}
}

View File

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

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

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

View File

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

View File

@@ -1,6 +1,5 @@
import { Certd } from '@certd/certd'
import DefaultPlugins from '@certd/plugins'
import { util } from '@certd/api'
import { pluginRegistry, util } from '@certd/api'
import _ from 'lodash-es'
import dayjs from 'dayjs'
import { Trace } from './trace.js'
@@ -18,59 +17,38 @@ function createDefaultOptions () {
}
export class Executor {
constructor () {
this.usePlugins(DefaultPlugins)
this.trace = new Trace()
}
useProviders (providers) {
if (providers) {
_.forEach(item => {
Certd.use(item)
})
}
}
useProvider (provider) {
Certd.use(provider)
}
usePlugin (plugin) {
if (plugin == null) {
return
}
if (this.plugins == null) {
this.plugins = {}
}
this.plugins[plugin.name] = plugin
if (plugin.define) {
const define = plugin.define()
this.plugins[define.name] = plugin
}
}
usePlugins (plugins) {
if (plugins) {
_.forEach(plugins, item => {
this.usePlugin(item)
})
}
}
async run (options) {
logger.info('------------------- Cert-D ---------------------')
try {
this.transfer(options)
options = _.merge(createDefaultOptions(), options)
return await this.doRun(options)
} catch (e) {
logger.error('任务执行出错', e)
logger.error('任务执行出错', e)
throw e
}
}
transfer (options) {
const providers = options.accessProviders
if (_.isArray(providers)) {
const map = {}
for (const provider of providers) {
if (provider.key) {
map[provider.key] = provider
}
}
options.accessProviders = map
}
}
async doRun (options) {
// 申请证书
logger.info('任务开始')
const certd = new Certd(options, this.providers)
const certd = new Certd(options)
const cert = await this.runCertd(certd)
if (cert == null) {
throw new Error('申请证书失败')
@@ -80,8 +58,11 @@ export class Executor {
logger.info('----------------------')
if (!cert.isNew) {
// 如果没有更新
if (!options.args.forceDeploy && !options.args.forceRedeploy) {
// 且不需要强制运行deploy
if (options.args.forceRedeploy) {
// 强制重新部署,清空保存的状态
await certd.certStore.setCurrentFile('context.json', '{}')
} else if (!options.args.forceDeploy) {
// 且不需要强制deploy
logger.info('证书无更新,无需重新部署')
logger.info('任务完成')
return { cert }
@@ -107,15 +88,16 @@ export class Executor {
logger.info('任务完成')
trace.print()
const result = resultTrace.get({ })
const returnData = {
if (result) {
if (result.status === 'error' && options.args.doNotThrowError === false) {
throw new Error(result.remark)
}
}
return {
cert,
context,
result
}
if (result.status === 'error' && options.args.doNotThrowError === false) {
throw new Error(result.remark)
}
return returnData
}
async runCertd (certd) {
@@ -140,6 +122,7 @@ export class Executor {
logger.info('此流程已被禁用,跳过')
logger.info('')
deployTrace.set({ value: { current: 'skip', status: 'disabled', remark: '流程禁用' } })
deployTrace.set({ tasks: null })
continue
}
try {
@@ -153,6 +136,7 @@ export class Executor {
}
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 } })
@@ -165,7 +149,7 @@ export class Executor {
async runTask ({ options, task, cert, context, deploy, trace }) {
const taskType = task.type
const Plugin = this.plugins[taskType]
const Plugin = pluginRegistry.get(taskType)
const deployName = deploy.deployName
const taskName = task.taskName
if (Plugin == null) {

View File

@@ -76,7 +76,9 @@ export class Trace {
}
}
const result = this.get({ type: 'result' })
this.printTraceLine(result, 'result', '')
if (result) {
this.printTraceLine(result, 'result', '')
}
const mainContext = {}
_.merge(mainContext, context)
delete mainContext.__trace__

View File

@@ -1,8 +1,16 @@
import pkg from 'chai'
import { Executor } from '../src/index.js'
import { createOptions } from '../../../test/options.js'
import { createOptions } from '../../../../test/options.js'
import PluginAliyun from '@certd/plugin-aliyun'
import PluginTencent from '@certd/plugin-tencent'
import PluginHost from '@certd/plugin-host'
const { expect } = pkg
// 安装默认插件和授权提供者
PluginAliyun.install()
PluginTencent.install()
PluginHost.install()
describe('AutoDeploy', function () {
it('#run', async function () {
this.timeout(120000)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -2,20 +2,41 @@ import { AbstractDnsProvider } from '@certd/api'
import Core from '@alicloud/pop-core'
import _ from 'lodash-es'
export class AliyunDnsProvider extends AbstractDnsProvider {
constructor (dnsProviderConfig) {
super()
static define () {
return {
name: 'aliyun',
label: '阿里云',
desc: '',
input: {
accessProvider: {
label: '授权',
type: [String, Object],
desc: '需要aliyun类型的授权',
component: {
name: 'access-provider-selector',
filter: 'aliyun'
},
required: true
}
},
output: {
}
}
}
constructor (args) {
super(args)
const { props } = args
const accessProvider = this.getAccessProvider(props.accessProvider)
this.client = new Core({
accessKeyId: dnsProviderConfig.accessKeyId,
accessKeySecret: dnsProviderConfig.accessKeySecret,
accessKeyId: accessProvider.accessKeyId,
accessKeySecret: accessProvider.accessKeySecret,
endpoint: 'https://alidns.aliyuncs.com',
apiVersion: '2015-01-09'
})
}
static name () {
return 'aliyun'
}
async getDomainList () {
const params = {
RegionId: 'cn-hangzhou'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ export class K8sClient {
* @param opts = {namespace:default}
* @returns secretsList
*/
async getSecret (opts) {
async getSecret (opts = {}) {
const namespace = opts.namespace || 'default'
const secrets = await this.client.api.v1.namespaces(namespace).secrets.get()
return secrets
@@ -87,6 +87,11 @@ export class K8sClient {
})
}
async getIngressList (opts) {
const namespace = opts.namespace || 'default'
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses.get()
}
async getIngress (opts) {
const namespace = opts.namespace || 'default'
const ingressName = opts.ingressName

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,19 @@
{
"name": "@certd/plugins",
"version": "0.1.11",
"name": "@certd/plugin-tencent",
"version": "0.3.1",
"description": "",
"main": "./src/index.js",
"main": "src/index.js",
"type": "module",
"dependencies": {
"@alicloud/pop-core": "^1.7.10",
"@certd/api": "^0.1.11",
"@certd/api": "^0.3.1",
"@certd/plugin-common": "^0.3.1",
"dayjs": "^1.9.7",
"kubernetes-client": "^9.0.0",
"lodash-es": "^4.17.20",
"ssh2": "^0.8.9",
"tencentcloud-sdk-nodejs": "^4.0.44"
},
"devDependencies": {
"@certd/certd": "^0.3.1",
"chai": "^4.2.0",
"eslint": "^7.15.0",
"eslint-config-standard": "^16.0.2",
@@ -23,5 +23,6 @@
"mocha": "^8.2.1"
},
"author": "Greper",
"license": "MIT"
"license": "MIT",
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
import pkg from 'chai'
import { UploadCertToTencent } from '../../src/tencent/upload-to-tencent/index.js'
import { UploadCertToTencent } from '../../src/plugins/upload-to-tencent/index.js'
import { Certd } from '@certd/certd'
import { createOptions } from '../../../../test/options.js'
import { createOptions } from '../../../../../test/options.js'
const { expect } = pkg
describe('PluginUploadToTencent', function () {
it('#execute', async function () {
const options = createOptions()
const plugin = new UploadCertToTencent()
const plugin = new UploadCertToTencent(options)
options.args = { test: false }
options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['*.docmirror.cn']

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