Compare commits

...

48 Commits

Author SHA1 Message Date
xiaojunnuo
15467fc233 v0.1.10 2021-01-08 15:57:57 +08:00
xiaojunnuo
eec0fcdcf1 refactor: 1 2021-01-08 15:57:38 +08:00
xiaojunnuo
a41dee015e fix: 修复plugins为null的问题 2021-01-08 15:56:50 +08:00
xiaojunnuo
d1a74713ef v0.1.9 2021-01-08 15:49:48 +08:00
xiaojunnuo
813e9e71d7 refactor: 1 2021-01-08 15:49:16 +08:00
xiaojunnuo
3d08dce26e refactor: 1 2021-01-08 15:46:37 +08:00
xiaojunnuo
4739d75f4a refactor: 1 2021-01-08 15:45:31 +08:00
xiaojunnuo
30cd62664b refactor: 1 2021-01-08 15:39:23 +08:00
xiaojunnuo
ae6b0fb111 v0.1.8 2021-01-08 15:07:15 +08:00
xiaojunnuo
9ec48f6ab8 refactor: tke ingress 内网配置 2021-01-08 15:06:49 +08:00
xiaojunnuo
f68565f444 refactor: tke ingress 内网配置 2021-01-08 15:04:58 +08:00
xiaojunnuo
ce5aae3795 refactor: tke ingress 内网配置 2021-01-08 13:01:35 +08:00
xiaojunnuo
bd00c09da0 v0.1.7 2021-01-08 10:33:11 +08:00
xiaojunnuo
44326c3abe refactor: home环境变量 2021-01-08 10:32:58 +08:00
xiaojunnuo
2f3db7d982 refactor: home环境变量 2021-01-08 10:25:58 +08:00
xiaojunnuo
9e4e3044b4 refactor: 1 2021-01-08 09:41:36 +08:00
xiaojunnuo
23bf0d07f7 refactor: 1 2021-01-08 09:37:02 +08:00
xiaojunnuo
6e61f8bcfb refactor: 1 2021-01-08 09:36:07 +08:00
xiaojunnuo
c7b28feb07 fix: 1 2021-01-08 09:13:29 +08:00
xiaojunnuo
624eade9f2 refactor: 排除certd-run 2021-01-07 18:06:42 +08:00
xiaojunnuo
d727a77289 refactor: 优化 2021-01-07 18:05:38 +08:00
xiaojunnuo
c0c2cb328c refactor: 小修改 2021-01-06 23:55:26 +08:00
xiaojunnuo
8cc80deff8 perf: 抽取api 2021-01-06 23:52:10 +08:00
xiaojunnuo
5312c11472 perf: 抽取api 2021-01-06 23:29:10 +08:00
xiaojunnuo
f07ce6f47d perf: 优化证书申请过程 2021-01-06 22:44:04 +08:00
xiaojunnuo
259e797ea5 refactor: 排除dev依赖 2021-01-05 22:25:14 +08:00
xiaojunnuo
d77addd2dc refactor: rollup 2021-01-05 21:27:23 +08:00
xiaojunnuo
c2420edb5a refactor: wepack 2021-01-04 00:45:04 +08:00
xiaojunnuo
e1396bb107 refactor: version 2021-01-03 23:35:03 +08:00
xiaojunnuo
b0def03790 refactor: 修复几个小bug 2021-01-03 23:33:51 +08:00
xiaojunnuo
9f371df372 refactor: version 2021-01-03 21:58:05 +08:00
xiaojunnuo
986fd4b010 feat: webpack pre 2021-01-03 21:50:14 +08:00
xiaojunnuo
4cd7b02cb7 feat: 上传证书到服务器,执行远程脚本 2021-01-03 02:30:34 +08:00
xiaojunnuo
67bff28255 feat: 腾讯云证书tke ingress 2021-01-02 02:47:58 +08:00
xiaojunnuo
43e90503ca feat: 腾讯云证书clb支持与删除 2020-12-28 00:22:12 +08:00
xiaojunnuo
25dae3d1ec feat: 腾讯云cdn支持 2020-12-26 01:37:53 +08:00
xiaojunnuo
ec81572670 refactor: dnspod支持 2020-12-24 00:58:12 +08:00
xiaojunnuo
71f6d5769a refactor: dnspod支持 2020-12-24 00:49:31 +08:00
xiaojunnuo
49b96592a0 refactor: 优化 2020-12-22 22:47:07 +08:00
xiaojunnuo
af0875ac4c refactor: delete 2020-12-21 00:34:53 +08:00
xiaojunnuo
48192b9002 refactor: delete 2020-12-21 00:33:52 +08:00
xiaojunnuo
ca289d4604 refactor: ignore 2020-12-21 00:32:50 +08:00
xiaojunnuo
6529fd2fdb feat: 自动化流程 2020-12-21 00:32:17 +08:00
xiaojunnuo
e84f5e8b0a refactor: 111 2020-12-19 02:00:46 +08:00
xiaojunnuo
df9f561fd3 refactor: ``` 2020-12-19 01:57:52 +08:00
xiaojunnuo
06603759fd feat: deployFlow 2020-12-16 00:35:05 +08:00
xiaojunnuo
a4bd29e6bf feat: 多域名绑定一张证书 2020-12-15 01:09:41 +08:00
xiaojunnuo
458486dd6b feat: 自动申请证书 2020-12-13 23:06:17 +08:00
75 changed files with 31824 additions and 1 deletions

6
.gitignore vendored
View File

@@ -4,4 +4,8 @@
out
gen
node_modules/
packages/*/test/*-private.js
/test/*.private.*
/other/node-acme-client/.idea/
/*.log
/other/certd-run
/other/node-acme-client

0
README.md Normal file
View File

6
lerna.json Normal file
View File

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

15
package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "root",
"private": true,
"type": "module",
"devDependencies": {
"lerna": "^3.18.4"
},
"scripts": {
"submodule": "git submodule update --init --recursive"
},
"license": "MIT",
"dependencies": {
"lodash": "^4.17.20"
}
}

14
packages/api/.eslintrc Normal file
View File

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

2468
packages/api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
packages/api/package.json Normal file
View File

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

View File

@@ -0,0 +1,34 @@
import _ from 'lodash-es'
import logger from '../utils/util.log.js'
export class AbstractDnsProvider {
constructor () {
this.logger = logger
}
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
}
}

View File

@@ -0,0 +1,4 @@
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'

View File

@@ -0,0 +1,72 @@
import fs from 'fs'
import logger from '../utils/util.log.js'
import dayjs from 'dayjs'
import Sleep from '../utils/util.sleep.js'
export class AbstractPlugin {
constructor ({ accessProviders }) {
this.logger = logger
this.accessProviders = accessProviders
}
appendTimeSuffix (name) {
if (name == null) {
name = 'certd'
}
return name + '-' + dayjs().format('YYYYMMDD-HHmmss')
}
async executeFromContextFile (options = {}) {
const { contextPath } = options
const contextJson = fs.readFileSync(contextPath)
const context = JSON.parse(contextJson)
options.context = context
await this.doExecute(options)
fs.writeFileSync(JSON.stringify(context))
}
async doExecute (options) {
try {
return await this.execute(options)
} catch (e) {
logger.error('插件执行出错:', e)
throw e
}
}
/**
* 执行
* @param options
* @returns {Promise<void>}
*/
async execute (options) {
console.error('请实现此方法,context:', options.context)
}
async doRollback (options) {
try {
return await this.rollback(options)
} catch (e) {
logger.error('插件rollback出错', e)
throw e
}
}
/**
* 回退,如有必要
* @param options
*/
async rollback (options) {
console.error('请实现此方法,rollback:', options.context)
}
getAccessProvider (accessProvider, accessProviders = this.accessProviders) {
if (typeof accessProvider === 'string' && accessProviders) {
accessProvider = accessProviders[accessProvider]
}
return accessProvider
}
async sleep (time) {
await Sleep(time)
}
}

View File

@@ -0,0 +1,33 @@
export class Store {
set (key, value) {
}
get (key) {
}
buildKey (...keyItem) {
}
linkExists (linkPath) {
}
link (targetPath, linkPath) {
}
unlink (linkPath) {
}
/**
* 全路径
* @param key
*/
getActualKey (key) {
// return 前缀+key
}
}

View File

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

View File

@@ -0,0 +1,7 @@
import log4js from 'log4js'
log4js.configure({
appenders: { std: { type: 'stdout' } },
categories: { default: { appenders: ['std'], level: 'info' } }
})
const logger = log4js.getLogger('certd')
export default logger

View File

@@ -0,0 +1,9 @@
import path from 'path'
function getUserBasePath () {
const userHome = process.env.USERPROFILE || process.env.HOME
return path.resolve(userHome, './.certd')
}
export default {
getUserBasePath
}

View File

@@ -0,0 +1,57 @@
import axios from 'axios'
import qs from 'qs'
import logger from './util.log.js'
/**
* @description 创建请求实例
*/
function createService () {
// 创建一个 axios 实例
const service = axios.create()
// 请求拦截
service.interceptors.request.use(
config => {
if (config.formData) {
config.data = qs.stringify(config.formData, {
arrayFormat: 'indices',
allowDots: true
}) // 序列化请求参数
delete config.formData
}
return config
},
error => {
// 发送失败
logger.error(error)
return Promise.reject(error)
}
)
// 响应拦截
service.interceptors.response.use(
response => {
logger.info('http response:', JSON.stringify(response.data))
return response.data
},
error => {
// const status = _.get(error, 'response.status')
// switch (status) {
// case 400: error.message = '请求错误'; break
// case 401: error.message = '未授权,请登录'; break
// case 403: error.message = '拒绝访问'; break
// case 404: error.message = `请求地址出错: ${error.response.config.url}`; break
// case 408: error.message = '请求超时'; break
// case 500: error.message = '服务器内部错误'; break
// case 501: error.message = '服务未实现'; break
// case 502: error.message = '网关错误'; break
// case 503: error.message = '服务不可用'; break
// case 504: error.message = '网关超时'; break
// case 505: error.message = 'HTTP版本不受支持'; break
// default: break
// }
logger.error('请求出错:', error.response.config.url, error)
return Promise.reject(error)
}
)
return service
}
export const request = createService()

View File

@@ -0,0 +1,7 @@
export default function (timeout) {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, timeout)
})
}

14
packages/certd/.eslintrc Normal file
View File

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

2361
packages/certd/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
{
"name": "@certd/certd",
"version": "0.1.7",
"description": "",
"main": "./src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"type": "module",
"author": "Greper",
"license": "MIT",
"dependencies": {
"@certd/acme-client": "^0.1.6",
"@certd/api": "^0.1.7",
"@certd/providers": "^0.1.7",
"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"
}
}

199
packages/certd/src/acme.js Normal file
View File

@@ -0,0 +1,199 @@
import acme from '@certd/acme-client'
import _ from 'lodash-es'
import { util } from '@certd/api'
const logger = util.logger
export class AcmeService {
constructor (store) {
this.store = store
}
async getAccountConfig (email) {
let conf = this.store.get(this.buildAccountPath(email))
if (conf == null) {
conf = {}
} else {
conf = JSON.parse(conf)
}
return conf
}
buildAccountPath (email) {
return this.store.buildKey(email, 'account.json')
}
saveAccountConfig (email, conf) {
this.store.set(this.buildAccountPath(email), JSON.stringify(conf))
}
async getAcmeClient (email, isTest) {
const conf = await this.getAccountConfig(email)
if (conf.key == null) {
conf.key = await this.createNewKey()
this.saveAccountConfig(email, conf)
}
if (isTest == null) {
isTest = process.env.CERTD_MODE === 'test'
}
const client = new acme.Client({
directoryUrl: isTest ? acme.directory.letsencrypt.staging : acme.directory.letsencrypt.production,
accountKey: conf.key,
accountUrl: conf.accountUrl,
backoffAttempts: 20,
backoffMin: 5000,
backoffMax: 10000
})
if (conf.accountUrl == null) {
const accountPayload = { termsOfServiceAgreed: true, contact: [`mailto:${email}`] }
await client.createAccount(accountPayload)
conf.accountUrl = client.getAccountUrl()
this.saveAccountConfig(email, conf)
}
return client
}
async createNewKey () {
const key = await acme.forge.createPrivateKey()
return key.toString()
}
async challengeCreateFn (authz, challenge, keyAuthorization, dnsProvider) {
logger.info('Triggered challengeCreateFn()')
/* http-01 */
if (challenge.type === 'http-01') {
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`
const fileContents = keyAuthorization
logger.info(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`)
/* Replace this */
logger.info(`Would write "${fileContents}" to path "${filePath}"`)
// await fs.writeFileAsync(filePath, fileContents);
} else if (challenge.type === 'dns-01') {
/* dns-01 */
const dnsRecord = `_acme-challenge.${authz.identifier.value}`
const recordValue = keyAuthorization
logger.info(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`)
/* Replace this */
logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`)
return await dnsProvider.createRecord({
fullRecord: dnsRecord,
type: 'TXT',
value: recordValue
})
}
}
/**
* Function used to remove an ACME challenge response
*
* @param {object} authz Authorization object
* @param {object} challenge Selected challenge
* @param {string} keyAuthorization Authorization key
* @param recordItem challengeCreateFn create record item
* @param dnsProvider dnsProvider
* @returns {Promise}
*/
async challengeRemoveFn (authz, challenge, keyAuthorization, recordItem, dnsProvider) {
logger.info('Triggered challengeRemoveFn()')
/* http-01 */
if (challenge.type === 'http-01') {
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`
logger.info(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`)
/* Replace this */
logger.info(`Would remove file on path "${filePath}"`)
// await fs.unlinkAsync(filePath);
} else if (challenge.type === 'dns-01') {
const dnsRecord = `_acme-challenge.${authz.identifier.value}`
const recordValue = keyAuthorization
logger.info(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`)
/* Replace this */
logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`)
await dnsProvider.removeRecord({
fullRecord: dnsRecord,
type: 'TXT',
value: keyAuthorization,
record: recordItem
})
}
}
async order ({ email, domains, dnsProvider, dnsProviderCreator, csrInfo, isTest }) {
const client = await this.getAcmeClient(email, isTest)
let accountUrl
try {
accountUrl = client.getAccountUrl()
} catch (e) {
}
/* Create CSR */
const { commonName, altNames } = this.buildCommonNameByDomains(domains)
const [key, csr] = await acme.forge.createCsr({
commonName,
...csrInfo,
altNames
})
if (dnsProvider == null && dnsProviderCreator) {
dnsProvider = await dnsProviderCreator()
}
if (dnsProvider == null) {
throw new Error('dnsProvider 不能为空')
}
/* 自动申请证书 */
const crt = await client.auto({
csr,
email: email,
termsOfServiceAgreed: true,
challengePriority: ['dns-01'],
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider)
},
challengeRemoveFn: async (authz, challenge, keyAuthorization, recordItem) => {
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider)
}
})
// 保存账号url
if (!accountUrl) {
try {
accountUrl = client.getAccountUrl()
this.setAccountUrl(email, accountUrl)
} catch (e) {
logger.warn('保存accountUrl出错', e)
}
}
/* Done */
logger.debug(`CSR:\n${csr.toString()}`)
logger.debug(`Certificate:\n${crt.toString()}`)
logger.info('证书申请成功')
return { key, crt, csr }
}
buildCommonNameByDomains (domains) {
if (typeof domains === 'string') {
domains = domains.split(',')
}
if (domains.length === 0) {
throw new Error('domain can not be empty')
}
const ret = {
commonName: domains[0]
}
if (domains.length > 1) {
ret.altNames = _.slice(domains, 1)
}
return ret
}
}

147
packages/certd/src/index.js Normal file
View File

@@ -0,0 +1,147 @@
import { util, Store } from '@certd/api'
import { AcmeService } from './acme.js'
import { FileStore } from './store/file-store.js'
import { CertStore } from './store/cert-store.js'
import { DnsProviderFactory } from '@certd/providers'
import dayjs from 'dayjs'
import forge from 'node-forge'
const logger = util.logger
export class Certd {
constructor (options) {
this.options = options
this.email = options.cert.email
this.domains = options.cert.domains
this.domain = this.getMainDomain(options.cert.domains)
if (!(options.store instanceof Store)) {
this.store = new FileStore(options.store || {})
}
this.certStore = new CertStore({
store: this.store,
email: options.cert.email,
domain: this.domain
})
this.acme = new AcmeService(this.store)
}
getMainDomain (domains) {
if (domains == null) {
return null
}
if (typeof domains === 'string') {
return domains
}
if (domains.length > 0) {
return domains[0]
}
}
//
// buildDomainFileName (domains) {
// const domain = this.getMainDomain(domains)
// return domain.replace(/\*/g, '_')
// }
//
// buildCertDir (email, domains) {
// const domainFileName = this.buildDomainFileName(domains)
// return path.join(email, '/certs/', domainFileName)
// }
async certApply () {
let oldCert
try {
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) {
const accessProviders = options.accessProviders
const providerOptions = accessProviders[options.cert.dnsProvider]
return DnsProviderFactory.createByType(providerOptions.providerType, providerOptions)
}
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)
}
}
async readCurrentCert () {
const cert = await this.certStore.readCert()
if (cert == null) {
return null
}
const { detail, expires } = this.getCrtDetail(cert.crt)
const domain = this.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
}
}
}

View File

@@ -0,0 +1,92 @@
import dayjs from 'dayjs'
export class CertStore {
constructor ({ store, email, domain }) {
this.store = store
this.email = email
this.domain = domain
this.safetyDomain = this.getSafetyDomain(this.domain)
this.certsRootPath = this.store.buildKey(this.email, 'certs')
this.currentRootPath = this.store.buildKey(this.certsRootPath, this.safetyDomain, 'current')
}
// getAccountConfig () {
// return this.store.get(this.accountConfigKey)
// }
//
// setAccountConfig (email, account) {
// return this.store.set(this.accountConfigKey, account)
// }
buildNewCertRootPath (dir) {
if (dir == null) {
dir = dayjs().format('YYYY.MM.DD.HHmmss')
}
return this.store.buildKey(this.certsRootPath, this.safetyDomain, 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.link(newDir, this.currentRootPath)
return newDir
}
async readCert (dir) {
if (dir == null) {
dir = this.currentRootPath
}
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, '_')
}
getCurrentFile (file) {
const key = this.buildKey(this.currentRootPath, file)
return this.store.get(key)
}
setCurrentFile (file, value) {
const key = this.buildKey(this.currentRootPath, file)
return this.store.set(key, value)
}
}

View File

@@ -0,0 +1,66 @@
import { Store, util } from '@certd/api'
import path from 'path'
import fs from 'fs'
const logger = util.logger
export class FileStore extends Store {
constructor (opts) {
super()
if (opts.rootDir != null) {
this.rootDir = opts.rootDir
} else {
this.rootDir = util.path.getUserBasePath()
}
if (opts.test) {
this.rootDir = path.join(this.rootDir, '/test/')
}
}
getActualKey (key) {
// return 前缀+key
return this.getPathByKey(key)
}
buildKey (...keyItem) {
return path.join(...keyItem)
}
getPathByKey (key) {
return path.join(this.rootDir, key)
}
set (key, value) {
const filePath = this.getPathByKey(key)
const dir = path.dirname(filePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
fs.writeFileSync(filePath, value)
return filePath
}
get (key) {
const filePath = this.getPathByKey(key)
if (!fs.existsSync(filePath)) {
return null
}
return fs.readFileSync(filePath).toString()
}
link (targetPath, linkPath) {
targetPath = this.getPathByKey(targetPath)
linkPath = this.getPathByKey(linkPath)
if (fs.existsSync(linkPath)) {
try {
fs.unlinkSync(linkPath)
} catch (e) {
logger.error('unlink error:', e)
}
}
fs.symlinkSync(targetPath, linkPath, 'dir')
}
unlink (linkPath) {
linkPath = this.getPathByKey(linkPath)
fs.unlinkSync(linkPath)
}
}

View File

@@ -0,0 +1,18 @@
import pkg from 'chai'
import { createOptions } from '../../../../test/options.js'
import { Certd } from '../../src/index.js'
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
})
})

View File

@@ -0,0 +1,20 @@
import pkg from 'chai'
import { Certd } from '../../src/index.js'
import { createOptions } from '../../../../test/options.js'
const { expect } = pkg
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.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
})
})

View File

@@ -0,0 +1,88 @@
import chai from 'chai'
import { Certd } from '../src/index.js'
import { createOptions } from '../../../test/options.js'
const { expect } = chai
const fakeCrt = `-----BEGIN CERTIFICATE-----
MIIFSTCCBDGgAwIBAgITAPoZZk/LhVIyXoic2NnJyxubezANBgkqhkiG9w0BAQsF
ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0yMDEyMTQx
NjA1NTFaFw0yMTAzMTQxNjA1NTFaMBsxGTAXBgNVBAMMECouZG9jbWlycm9yLmNs
dWIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC75tGrYjly+RpcZehQ
my1EpaXElT4L60pINKV2YDKnBrcSSo1c6rO7nFh12eC/ju4WwYUep0RVmBDF8xD0
I1Sd1uuDTQWP0UT1X9yqdXtjvxpUqoCHAzG633f3sJRFul7mDLuC9tRCuae9o7qP
EZ827XOmjBR35dso9I2GEE4828J3YE3tSKtobZlM+30jozLEcsO0PTyM5mq5PPjP
VI3fGLcEaBmLZf5ixz4XkcY9IAhyAMYf03cT2wRoYPBaDdXblgCYL6sFtIMbzl3M
Di94PB8NyoNSsC2nmBdWi54wFOgBvY/4ljsX/q7X3EqlSvcA0/M6/c/J9kJ3eupv
jV8nAgMBAAGjggJ9MIICeTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFAkdTjSCV3KD
x28sf98MrwVfyFYgMB8GA1UdIwQYMBaAFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHcG
CCsGAQUFBwEBBGswaTAyBggrBgEFBQcwAYYmaHR0cDovL29jc3Auc3RnLWludC14
MS5sZXRzZW5jcnlwdC5vcmcwMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0Zy1p
bnQteDEubGV0c2VuY3J5cHQub3JnLzArBgNVHREEJDAighAqLmRvY21pcnJvci5j
bHVigg5kb2NtaXJyb3IuY2x1YjBMBgNVHSAERTBDMAgGBmeBDAECATA3BgsrBgEE
AYLfEwEBATAoMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNyeXB0Lm9y
ZzCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB1ABboacHRlerXw/iXGuPwdgH3jOG2
nTGoUhi2g38xqBUIAAABdmI3LM4AAAQDAEYwRAIgaiNqXSEq+sxp8eqlJXp/KFdO
so5mT50MoRsLF8Inu0ACIDP46+ekng7I0BlmyIPmbqFcZgnZFVWLLCdLYijhVyOL
AHcA3Zk0/KXnJIDJVmh9gTSZCEmySfe1adjHvKs/XMHzbmQAAAF2YjcuxwAABAMA
SDBGAiEAxpeB8/w4YkHZ62nH20h128VtuTSmYDCnF7EK2fQyeZYCIQDbJlF2wehZ
sF1BeE7qnYYqCTP0dYIrQ9HWtBa/MbGOKTANBgkqhkiG9w0BAQsFAAOCAQEAL2di
HKh6XcZtGk0BFxJa51sCZ3MLu9+Zy90kCRD4ooP5x932WxVM25+LBRd+xSzx+TRL
UVrlKp9GdMYX1JXL4Vf2NwzuFO3snPDe/qizD/3+D6yo8eKJ/LD82t5kLWAD2rto
YfVSTKwfNIBBJwHUnjviBPJmheHHCKmz8Ct6/6QxFAeta9TAMn0sFeVCQnmAq7HL
jrunq0tNHR/EKG0ITPLf+6P7MxbmpYNnq918766l0tKsW8oo8ZSGEwKU2LMaSiAa
hasyl/2gMnYXjtKOjDcnR8oLpbrOg0qpVbynmJin1HP835oHPPAZ1gLsqYTTizNz
AHxTaXliTVvS83dogw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
-----END CERTIFICATE-----`
describe('Certd', function () {
it('#buildCertDir', function () {
const options = createOptions()
options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['*.docmirror.club']
const certd = new Certd(options)
const currentRootPath = certd.certStore.currentRootPath
console.log('rootDir', currentRootPath)
expect(currentRootPath).match(/xiaojunnuo@qq.com\\certs\\_.docmirror.club\\current/)
})
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('expires:', cert.expires)
})
})

View File

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

3920
packages/executor/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
{
"name": "@certd/executor",
"version": "0.1.10",
"description": "",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.config.cjs ",
"rollup": "rollup --config rollup.config.js"
},
"type": "module",
"dependencies": {
"@certd/api": "^0.1.7",
"@certd/certd": "^0.1.7",
"@certd/plugins": "^0.1.9",
"dayjs": "^1.9.7",
"lodash-es": "^4.17.20"
},
"devDependencies": {
"@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
}

View File

@@ -0,0 +1,21 @@
import json from '@rollup/plugin-json'
import { terser } from 'rollup-plugin-terser'
import commonjs from '@rollup/plugin-commonjs'
import { nodeResolve } from '@rollup/plugin-node-resolve'
export default {
input: 'src/index.js',
output: [
{
file: 'bundle.js',
format: 'es'
},
{
file: 'bundle.min.js',
format: 'iife',
name: 'version',
plugins: [terser()]
}
],
plugins: [json(), commonjs(), nodeResolve()]
}

View File

@@ -0,0 +1,181 @@
import { Certd } from '@certd/certd'
import DefaultPlugins from '@certd/plugins'
import { 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
}
}
}
export class Executor {
constructor () {
this.usePlugins(DefaultPlugins)
this.trace = new Trace()
}
use (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) {
for (const plugin of plugins) {
this.use(plugin)
}
}
}
async run (options) {
logger.info('------------------- Cert-D ---------------------')
try {
options = _.merge(createDefaultOptions(), options)
return await this.doRun(options)
} catch (e) {
logger.error('任务执行出错:', e)
throw e
}
}
async doRun (options) {
// 申请证书
logger.info('任务开始')
const certd = new Certd(options)
const cert = await this.runCertd(certd)
if (cert == null) {
throw new Error('申请证书失败')
}
logger.info('证书保存路径:', cert.certDir)
logger.info('----------------------')
if (!cert.isNew) {
// 如果没有更新
if (!options.args?.forceDeploy && !options.args?.forceRedeploy) {
// 且不需要强制运行deploy
logger.info('证书无更新,无需重新部署')
logger.info('任务完成')
return { cert }
}
}
// 读取上次执行进度
let context = {}
const contextJson = await certd.certStore.getCurrentFile('context.json')
if (contextJson) {
context = JSON.parse(contextJson)
}
context.certIsNew = !!cert.isNew
const trace = new Trace(context)
// 运行部署任务
try {
await this.runDeploys({ options, cert, context, trace })
} finally {
await certd.certStore.setCurrentFile('context.json', JSON.stringify(context))
}
logger.info('任务完成')
trace.print()
const result = trace.get({ })
const returnData = {
cert,
context,
result
}
if (result.status === 'error') {
const err = new Error(result.remark)
err.data = returnData
}
return returnData
}
async runCertd (certd) {
logger.info(`证书任务 ${JSON.stringify(certd.options.cert.domains)} 开始`)
const cert = await certd.certApply()
logger.info(`证书任务 ${JSON.stringify(certd.options.cert.domains)} 完成`)
return cert
}
async runDeploys ({ options, cert, context, trace }) {
if (cert == null) {
const certd = new Certd(options)
cert = await certd.readCurrentCert()
}
logger.info('部署任务开始')
for (const deploy of options.deploy) {
const deployName = deploy.deployName
logger.info(`------------【${deployName}】-----------`)
if (deploy.disabled === true) {
logger.info('此流程已被禁用,跳过')
logger.info('')
trace.set({ deployName, value: { current: 'skip', status: 'disabled', remark: '流程禁用' } })
continue
}
try {
for (const task of deploy.tasks) {
if (context[deployName] == null) {
context[deployName] = {}
}
const taskContext = context[deployName]
await this.runTask({ options, cert, task, context: taskContext, deploy, trace })
}
trace.set({ deployName, value: { status: 'success', remark: '执行成功' } })
} catch (e) {
trace.set({ deployName, value: { status: 'error', remark: '执行失败:' + e.message } })
trace.set({ 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 = this.plugins[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 traceStatus = trace.get({ deployName: deploy.deployName, taskName: taskName })
if (traceStatus?.status === 'success' && !options?.args?.forceRedeploy) {
logger.info(`----【${taskName}】已经执行完成,跳过此任务`)
trace.set({ deployName, taskName, value: { current: 'skip', status: 'success', remark: '已执行成功过,本次跳过' } })
return
}
logger.info(`----【${taskName}】开始执行`)
try {
await instance.execute({ cert, props: task.props, context })
trace.set({ deployName, taskName, value: { current: 'success', status: 'success', remark: '执行成功', time: dayjs().format() } })
} catch (e) {
trace.set({ deployName, taskName, value: { current: 'error', status: 'error', remark: e.message, time: dayjs().format() } })
throw e
}
logger.info(`----任务【${taskName}】执行完成`)
logger.info('')
}
}

View File

@@ -0,0 +1,82 @@
import { util } from '@certd/api'
import _ from 'lodash-es'
const logger = util.logger
export class Trace {
constructor (context) {
this.context = context
}
set ({ deployName, taskName, prop, value }) {
const key = this.buildTraceKey({ deployName, taskName, prop })
const oldValue = _.get(this.context, key) || {}
_.merge(oldValue, value)
_.set(this.context, key, oldValue)
}
get ({ deployName, taskName, prop }) {
return _.get(this.context, this.buildTraceKey({ deployName, taskName, prop }))
}
buildTraceKey ({ deployName, taskName, prop }) {
let key = '__trace__'
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({ })
// 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 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
}
}

View File

@@ -0,0 +1,31 @@
import pkg from 'chai'
import { Executor } from '../src/index.js'
import { createOptions } from '../../../test/options.js'
const { expect } = pkg
describe('AutoDeploy', function () {
it('#run', async function () {
this.timeout(20000)
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(20000)
const executor = new Executor()
const options = createOptions()
const ret = await executor.run(options, { forceCert: true, forceDeploy: false })
expect(ret).ok
expect(ret.cert).ok
})
it('#forceDeploy', async function () {
this.timeout(20000)
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
})
})

View File

@@ -0,0 +1,23 @@
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
console.log(CleanWebpackPlugin)
module.exports = {
devtool: 'source-map',
target: 'node',
entry: './src/index.js',
output: {
filename: 'executor.js',
path: path.resolve(__dirname, 'dist'),
library: 'certdExecutor',
libraryTarget: 'umd'
},
plugins: [
new CleanWebpackPlugin()
],
mode: 'production'
// mode: 'development',
// optimization: {
// usedExports: true
// }
}

3136
packages/executor/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

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

3757
packages/plugins/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,9 @@
import { AbstractPlugin } from '@certd/api'
export class AbstractAliyunPlugin extends AbstractPlugin {
checkRet (ret) {
if (ret.code != null) {
throw new Error('执行失败:', ret.Message)
}
}
}

View File

@@ -0,0 +1,94 @@
import { AbstractAliyunPlugin } from '../../aliyun/abstract-aliyun.js'
import Core from '@alicloud/pop-core'
import dayjs from 'dayjs'
export class DeployCertToAliyunCDN extends AbstractAliyunPlugin {
/**
* 插件定义
* 名称
* 入参
* 出参
*/
static define () {
return {
name: 'deployCertToAliyunCDN',
label: '部署到阿里云CDN',
input: {
domainName: {
label: 'cdn加速域名',
required: true
},
certName: {
label: '证书名称'
},
from: {
value: 'upload',
label: '证书来源',
options: [
{ value: 'upload', label: '直接上传' },
{ value: 'cas', label: '从证书库', desc: '需要uploadCertToAliyun作为前置任务' }
],
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]',
required: true
}
},
output: {
}
}
}
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

@@ -0,0 +1,97 @@
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]'
}
},
output: {
aliyunCertId: {
type: String,
desc: '上传成功后的阿里云CertId'
}
}
}
}
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
}
}

View File

@@ -0,0 +1,9 @@
import { AbstractPlugin } from '@certd/api'
export class AbstractHostPlugin extends AbstractPlugin {
checkRet (ret) {
if (ret.code != null) {
throw new Error('执行失败:', ret.Message)
}
}
}

View File

@@ -0,0 +1,51 @@
import { AbstractHostPlugin } from '../abstract-host.js'
import { SshClient } from '../ssh.js'
export class HostShellExecute extends AbstractHostPlugin {
/**
* 插件定义
* 名称
* 入参
* 出参
*/
static define () {
return {
name: 'hostShellExecute',
label: '执行远程主机脚本命令',
input: {
script: {
label: 'shell脚本命令'
},
accessProvider: {
label: '主机登录配置',
type: [String, Object],
desc: 'AccessProviders的key 或 一个包含用户名密码的对象',
options: 'accessProviders[type=ssh]'
}
},
output: {
}
}
}
async execute ({ cert, props, context }) {
const { script, accessProvider } = props
const connectConf = this.getAccessProvider(accessProvider)
const sshClient = new SshClient()
const ret = await sshClient.shell({
connectConf,
script
})
return ret
}
/**
* @param cert
* @param props
* @param context
* @returns {Promise<void>}
*/
async rollback ({ cert, props, context }) {
}
}

View File

@@ -0,0 +1,110 @@
import ssh2 from 'ssh2'
import logger from '../utils/util.log.js'
import path from 'path'
export class SshClient {
/**
*
* @param connectConf
{
host: '192.168.100.100',
port: 22,
username: 'frylock',
password: 'nodejsrules'
}
* @param transports
*/
uploadFiles ({ connectConf, transports }) {
const conn = new ssh2.Client()
return new Promise((resolve, reject) => {
conn.on('ready', () => {
logger.info('连接服务器成功')
conn.sftp(async (err, sftp) => {
if (err) {
throw err
}
try {
for (const transport of transports) {
logger.info('上传文件:', JSON.stringify(transport))
await this.exec({ conn, cmd: 'mkdir ' + path.dirname(transport.remotePath) })
await this.fastPut({ sftp, ...transport })
}
resolve()
} catch (e) {
reject(e)
} finally {
conn.end()
}
})
}).connect(connectConf)
})
}
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()
})
})
}
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

@@ -0,0 +1,77 @@
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: 'AccessProviders的key 或 一个包含用户名密码的对象',
options: 'accessProviders[type=ssh]'
}
},
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 }) {
}
}

View File

@@ -0,0 +1,19 @@
import { UploadCertToAliyun } from './aliyun/upload-to-aliyun/index.js'
import { DeployCertToAliyunCDN } from './aliyun/deploy-to-cdn/index.js'
import { UploadCertToTencent } from './tencent/upload-to-tencent/index.js'
import { DeployCertToTencentCDN } from './tencent/deploy-to-cdn/index.js'
import { DeployCertToTencentCLB } from './tencent/deploy-to-clb/index.js'
import { DeployCertToTencentTKEIngress } from './tencent/deploy-to-tke-ingress/index.js'
export default [
UploadCertToAliyun,
DeployCertToAliyunCDN,
UploadCertToTencent,
DeployCertToTencentTKEIngress,
DeployCertToTencentCDN,
DeployCertToTencentCLB
]

View File

@@ -0,0 +1,13 @@
import { AbstractPlugin } from '@certd/api'
export class AbstractTencentPlugin extends AbstractPlugin {
checkRet (ret) {
if (!ret || ret.Error) {
throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message)
}
}
getSafetyDomain (domain) {
return domain.replace(/\*/g, '_')
}
}

View File

@@ -0,0 +1,115 @@
import { AbstractTencentPlugin } from '../../tencent/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: '证书名称'
},
certType: {
value: 'upload',
label: '证书来源',
options: [
{ value: 'upload', label: '直接上传' },
{ value: 'cloud', label: '从证书库', desc: '需要uploadCertToTencent作为前置任务' }
],
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]',
required: true
}
},
output: {
}
}
}
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
}
}

View File

@@ -0,0 +1,191 @@
import { AbstractTencentPlugin } from '../../tencent/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: '大区',
value: 'ap-guangzhou'
},
domain: {
label: '域名',
type: [String, Array],
desc: '要更新的支持https的负载均衡的域名'
},
loadBalancerId: {
label: '负载均衡ID',
desc: '如果没有配置则根据域名匹配负载均衡下的监听器根据域名匹配时暂时只支持前100个'
},
listenerId: {
label: '监听器ID',
desc: '如果没有配置则根据域名或负载均衡id匹配监听器'
},
certName: {
label: '证书名称',
desc: '如无uploadCertToTencent作为前置则此项需要设置默认为域名'
},
accessProvider: {
label: 'Access提供者',
type: [String, Object],
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
options: 'accessProviders[type=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)
}
}

View File

@@ -0,0 +1,170 @@
import { AbstractTencentPlugin } from '../../tencent/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: '大区',
value: 'ap-guangzhou'
},
clusterId: {
label: '集群ID',
required: true,
desc: '例如cls-6lbj1vee'
},
namespace: {
label: '集群的namespace',
value: 'default'
},
secreteName: {
type: [String, Array],
label: '证书的secret名称',
desc: '支持多个(传入数组)'
},
ingressName: {
type: [String, Array],
label: 'ingress名称',
desc: '支持多个(传入数组)'
},
clusterIp: {
type: String,
label: '集群内网ip',
desc: '如果开启了外网的话,无需设置'
},
clusterDomain: {
type: String,
label: '集群域名,可不填,默认为:[clusterId].ccs.tencent-cloud.com'
},
/**
* AccessProvider的key,或者一个包含access的具体的对象
*/
accessProvider: {
label: 'Access提供者',
type: [String, Object],
desc: '请选择access提供者',
component: {
name: 'accessProviderSelect',
props: {
filterType: 'tencent'
}
},
required: true
}
},
output: {
}
}
}
async execute ({ cert, props, context }) {
const accessProvider = this.getAccessProvider(props.accessProvider)
const tkeClient = this.getTkeClient(accessProvider, props.region)
const kubeConfigStr = await this.getTkeKubeConfig(tkeClient, props.clusterId)
this.logger.info('kubeconfig已成功获取')
const k8sClient = new K8sClient(kubeConfigStr)
if (props.clusterIp != null) {
let clusterDomain = props.clusterDomain
if (!clusterDomain) {
clusterDomain = `${props.clusterId}.ccs.tencent-cloud.com`
}
// 修改内网解析ip地址
k8sClient.setLookup({ [clusterDomain]: { ip: props.clusterIp } })
}
await this.patchCertSecret({ k8sClient, props, context })
await this.sleep(2000) // 停留2秒等待secret部署完成
await this.restartIngress({ k8sClient, props })
return true
}
getTkeClient (accessProvider, region = 'ap-guangzhou') {
const TkeClient = tencentcloud.tke.v20180525.Client
const clientConfig = {
credential: {
secretId: accessProvider.secretId,
secretKey: accessProvider.secretKey
},
region,
profile: {
httpProfile: {
endpoint: 'tke.tencentcloudapi.com'
}
}
}
return new TkeClient(clientConfig)
}
async getTkeKubeConfig (client, clusterId) {
// Depends on tencentcloud-sdk-nodejs version 4.0.3 or higher
const params = {
ClusterId: clusterId
}
const ret = await client.DescribeClusterKubeconfig(params)
this.checkRet(ret)
this.logger.info('注意:后续操作需要在【集群->基本信息】中开启外网或内网访问,https://console.cloud.tencent.com/tke2/cluster')
return ret.Kubeconfig
}
async patchCertSecret ({ k8sClient, props, context }) {
const { tencentCertId } = context
if (tencentCertId == null) {
throw new Error('请先将【上传证书到腾讯云】作为前置任务')
}
this.logger.info('腾讯云证书ID:', tencentCertId)
const certIdBase64 = Buffer.from(tencentCertId).toString('base64')
const { namespace, secretName } = props
const body = {
data: {
qcloud_cert_id: certIdBase64
},
metadata: {
labels: {
certd: this.appendTimeSuffix('certd')
}
}
}
let secretNames = secretName
if (typeof secretName === 'string') {
secretNames = [secretName]
}
for (const secret of secretNames) {
await k8sClient.patchSecret({ namespace, secretName: secret, body })
this.logger.info(`CertSecret已更新:${secret}`)
}
}
async restartIngress ({ k8sClient, props }) {
const { namespace, ingressName } = props
const body = {
metadata: {
labels: {
certd: this.appendTimeSuffix('certd')
}
}
}
let ingressNames = ingressName
if (typeof ingressName === 'string') {
ingressNames = [ingressName]
}
for (const ingress of ingressNames) {
await k8sClient.patchIngress({ namespace, ingressName: ingress, body })
this.logger.info(`ingress已重启:${ingress}`)
}
}
}

View File

@@ -0,0 +1,87 @@
import dayjs from 'dayjs'
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: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
options: 'accessProviders[type=tencent]'
}
},
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
}
}

View File

@@ -0,0 +1,109 @@
import kubernetesClient from 'kubernetes-client'
import { util } from '@certd/api'
import Request from 'kubernetes-client/backends/request/index.js'
import dns from 'dns'
const { KubeConfig, Client } = kubernetesClient
const logger = util.logger
export class K8sClient {
constructor (kubeConfigStr) {
this.kubeConfigStr = kubeConfigStr
this.init()
}
init () {
const kubeconfig = new KubeConfig()
kubeconfig.loadFromString(this.kubeConfigStr)
const reqOpts = { kubeconfig, request: {} }
if (this.lookup) {
reqOpts.request.lookup = this.lookup
}
const backend = new Request(reqOpts)
this.client = new Client({ backend, version: '1.13' })
}
/**
*
* @param localRecords { [domain]:{ip:'xxx.xx.xxx'} }
*/
setLookup (localRecords) {
this.lookup = (hostnameReq, options, callback) => {
logger.info('custom lookup', hostnameReq, localRecords)
if (localRecords[hostnameReq]) {
logger.info('local record', hostnameReq, localRecords[hostnameReq])
callback(null, localRecords[hostnameReq].ip, 4)
} else {
dns.lookup(hostnameReq, options, callback)
}
}
this.init()
}
/**
* 查询 secret列表
* @param opts = {namespace:default}
* @returns secretsList
*/
async getSecret (opts) {
const namespace = opts?.namespace || 'default'
const secrets = await this.client.api.v1.namespaces(namespace).secrets.get()
return secrets
}
/**
* 创建Secret
* @param opts {namespace:default, body:yamlStr}
* @returns {Promise<*>}
*/
async createSecret (opts) {
const namespace = opts?.namespace || 'default'
const created = await this.client.api.v1.namespaces(namespace).secrets.post({
body: opts.body
})
logger.info('new secrets:', created)
return created
}
async updateSecret (opts) {
const namespace = opts?.namespace || 'default'
const secretName = opts?.secretName
if (secretName == null) {
throw new Error('secretName 不能为空')
}
return await this.client.api.v1.namespaces(namespace).secrets(secretName).put({
body: opts.body
})
}
async patchSecret (opts) {
const namespace = opts?.namespace || 'default'
const secretName = opts?.secretName
if (secretName == null) {
throw new Error('secretName 不能为空')
}
return await this.client.api.v1.namespaces(namespace).secrets(secretName).patch({
body: opts.body
})
}
async getIngress (opts) {
const namespace = opts?.namespace || 'default'
const ingressName = opts?.ingressName
if (!ingressName) {
throw new Error('ingressName 不能为空')
}
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).get()
}
async patchIngress (opts) {
const namespace = opts?.namespace || 'default'
const ingressName = opts?.ingressName
if (!ingressName) {
throw new Error('ingressName 不能为空')
}
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).patch({
body: opts.body
})
}
}

View File

@@ -0,0 +1,21 @@
import pkg from 'chai'
import { DeployCertToAliyunCDN } from '../../src/aliyun/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.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' }
})
console.log('context:', context, ret)
})
})

View File

@@ -0,0 +1,28 @@
import pkg from 'chai'
import { UploadCertToAliyun } from '../../src/aliyun/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()
const certd = new Certd(options)
const cert = await certd.readCurrentCert()
const context = {}
const deployOpts = {
accessProviders: options.accessProviders,
cert,
props: { accessProvider: 'aliyun' },
context
}
await plugin.doExecute(deployOpts)
console.log('context:', context)
// await plugin.sleep(1000)
// await plugin.rollback(deployOpts)
})
})

View File

@@ -0,0 +1,29 @@
import pkg from 'chai'
import { HostShellExecute } from '../../src/host/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 ', accessProvider: 'aliyun-ssh' },
context
}
const ret = await plugin.doExecute(uploadOpts)
for (const retElement of ret) {
console.log('-----' + retElement)
}
await plugin.doRollback(uploadOpts)
})
})

View File

@@ -0,0 +1,27 @@
import pkg from 'chai'
import { UploadCertToHost } from '../../src/host/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)
})
})

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: '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,54 @@
import pkg from 'chai'
import { DeployCertToTencentCDN } from '../../src/tencent/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'
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 context = {}
const uploadPlugin = new UploadCertToTencent()
const uploadOptions = {
accessProviders: options.accessProviders,
cert,
props: { name: 'certd部署测试', accessProvider: 'tencent' },
context
}
await uploadPlugin.doExecute(uploadOptions)
const deployPlugin = new DeployCertToTencentCDN()
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
console.log('context:', context)
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()
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)
})
})

View File

@@ -0,0 +1,107 @@
import pkg from 'chai'
import { DeployCertToTencentCLB } from '../../src/tencent/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'
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 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 { 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'
}
const accessProvider = deployPlugin.getAccessProvider(props.accessProvider)
const { region } = props
const client = deployPlugin.getClient(accessProvider, region)
const ret = await deployPlugin.getListenerList(client, props.loadBalancerId, props)
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()
const context = {}
const deployOpts = {
accessProviders: options.accessProviders,
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()
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()
const context = {}
const deployOpts = {
accessProviders: options.accessProviders,
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)
expect(ret).ok
console.log('ret:', ret)
// 删除测试证书
const uploadPlugin = new UploadCertToTencent()
await uploadPlugin.doRollback(deployOpts)
})
})

View File

@@ -0,0 +1,114 @@
import pkg from 'chai'
import { DeployCertToTencentTKEIngress } from '../../src/tencent/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('#getTkeKubeConfig', async function () {
// const { options, deployOpts } = await getOptions()
// const plugin = new DeployCertToTencentTKEIngress()
// const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
// const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props)
// console.log('kubeConfig:', kubeConfig)
// })
//
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()
console.log('secrets:', secrets)
})
//
// it('#patchTKECertSecrets', async function () {
// this.timeout(5000)
//
// const { options, deployOpts } = await getOptions()
// const plugin = new DeployCertToTencentTKEIngress()
// const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
// const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props)
// const k8sClient = new K8sClient(kubeConfig)
//
// deployOpts.k8sClient = k8sClient
// deployOpts.context.tencentCertId = 'hNVD3Z45'
// const newCecret = await plugin.patchCertSecret(deployOpts)
// console.log('newCecret', newCecret)
// })
// it('#GetTkeIngress', async function () {
// this.timeout(5000)
//
// const { options, deployOpts } = await getOptions()
// deployOpts.props.ingressName = 'ingress-base'
// deployOpts.props.secretName = 'cert---docmirror-cn'
// const plugin = new DeployCertToTencentTKEIngress()
// const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
// const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props)
//
// const k8sClient = new K8sClient(kubeConfig)
// const ingress = await k8sClient.getIngress({
// ingressName: 'ingress-base'
// })
// console.log('ingress:', ingress)
// })
// it('#RestartTKEIngress', async function () {
// this.timeout(5000)
//
// const { options, deployOpts } = await getOptions()
// deployOpts.props.ingressName = 'ingress-base'
// deployOpts.props.secretName = 'cert---docmirror-cn'
// const plugin = new DeployCertToTencentTKEIngress()
// const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
// const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props)
//
// const k8sClient = new K8sClient(kubeConfig)
//
// deployOpts.k8sClient = k8sClient
// deployOpts.context.tencentCertId = 'hNVD3Z45'
// const newCecret = await plugin.restartIngress(deployOpts)
// console.log('newCecret', newCecret)
// })
it('#execute', async function () {
this.timeout(5000)
const { deployOpts } = await getOptions()
deployOpts.props.ingressName = 'ingress-base'
deployOpts.props.secretName = 'cert---docmirror-cn'
deployOpts.context.tencentCertId = 'hNUZJrZf'
const plugin = new DeployCertToTencentTKEIngress()
const ret = await plugin.doExecute(deployOpts)
console.log('sucess', ret)
})
})

View File

@@ -0,0 +1,27 @@
import pkg from 'chai'
import { UploadCertToTencent } from '../../src/tencent/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.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

2922
packages/plugins/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

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

2814
packages/providers/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,12 @@
import dnsProviders from './index.js'
export class DnsProviderFactory {
static createByType (type, options) {
try {
const Provider = dnsProviders[type]
return new Provider(options)
} catch (e) {
throw new Error('暂不支持此dnsProvider' + type, e)
}
}
}

View File

@@ -0,0 +1,108 @@
import { AbstractDnsProvider } from '@certd/api'
import Core from '@alicloud/pop-core'
import _ from 'lodash-es'
export class AliyunDnsProvider extends AbstractDnsProvider {
constructor (dnsProviderConfig) {
super()
this.client = new Core({
accessKeyId: dnsProviderConfig.accessKeyId,
accessKeySecret: dnsProviderConfig.accessKeySecret,
endpoint: 'https://alidns.aliyuncs.com',
apiVersion: '2015-01-09'
})
}
static name () {
return 'aliyun'
}
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) {
// e.code === 'DomainRecordDuplicate'
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
}
}

View File

@@ -0,0 +1,79 @@
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')
}
this.loginToken = dnsProviderConfig.id + ',' + dnsProviderConfig.token
}
async doRequest (options) {
const config = {
method: 'post',
formData: {
login_token: this.loginToken,
format: 'json',
lang: 'cn',
error_on_empty: 'no'
},
timeout: 5000
}
_.merge(config, options)
const ret = await request(config)
if (ret?.status?.code !== '1') {
throw new Error('请求失败:' + ret.status.message + ',api=' + config.url)
}
return ret
}
async getDomainList () {
const ret = await this.doRequest({
url: 'https://dnsapi.cn/Domain.List'
})
this.logger.debug('dnspod 域名列表:', ret.domains)
return ret.domains
}
async createRecord ({ fullRecord, type, value }) {
this.logger.info('添加域名解析:', fullRecord, value)
const domainItem = await this.matchDomain(fullRecord, 'name')
const domain = domainItem.name
const rr = fullRecord.replace('.' + domain, '')
const ret = await this.doRequest({
url: 'https://dnsapi.cn/Record.Create',
formData: {
domain,
sub_domain: rr,
record_type: type,
record_line: '默认',
value: value,
mx: 1
}
})
this.logger.info('添加域名解析成功:', fullRecord, value, JSON.stringify(ret.record))
return ret.record
}
async removeRecord ({ fullRecord, type, value, record }) {
const domain = await this.matchDomain(fullRecord, 'name')
const ret = await this.doRequest({
url: 'https://dnsapi.cn/Record.Remove',
formData: {
domain,
record_id: record.id
}
})
this.logger.info('删除域名解析成功:', fullRecord, value)
return ret.RecordId
}
}

View File

@@ -0,0 +1,6 @@
import { AliyunDnsProvider } from './impl/aliyun.js'
import { DnspodDnsProvider } from './impl/dnspod.js'
export default {
[AliyunDnsProvider.name()]: AliyunDnsProvider,
[DnspodDnsProvider.name()]: DnspodDnsProvider
}

View File

@@ -0,0 +1,4 @@
import dnsProviders from './dns-provider/index.js'
export { DnsProviderFactory } from './dns-provider/dns-provider-factory.js'
export default dnsProviders

View File

@@ -0,0 +1,34 @@
import pkg from 'chai'
import AliyunDnsProvider from '../../src/dns-provider/impl/aliyun.js'
import { createOptions } from '../../../../test/options.js'
import { Certd } from '../../src/index.js'
const { expect } = pkg
describe('AliyunDnsProvider', function () {
it('#getDomainList', async function () {
const options = createOptions()
const aliyunDnsProvider = new AliyunDnsProvider(options.accessProviders.aliyun)
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 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 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
})
})

View File

@@ -0,0 +1,24 @@
import pkg from 'chai'
import DnspodDnsProvider from '../../src/dns-provider/impl/dnspod.js'
import { Certd } from '../../src/index.js'
import { createOptions } from '../../../../test/options.js'
const { expect } = pkg
describe('DnspodDnsProvider', function () {
it('#getDomainList', async function () {
const options = createOptions()
const dnsProvider = new DnspodDnsProvider(options.accessProviders.dnspod)
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)
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 })
})
})

138
test/options.js Normal file
View File

@@ -0,0 +1,138 @@
import _ from 'lodash'
import optionsPrivate from './options.private.js'
const defaultOptions = {
args: {
forceCert: false, // 强制更新证书
skipCert: false, // 是否跳过证书申请环节
forceDeploy: false,
test: true
},
accessProviders: {
aliyun: {
providerType: 'aliyun',
accessKeyId: '',
accessKeySecret: ''
},
dnspod: {
providerType: 'dnspod',
id: '',
token: ''
},
tencent: {
providerType: 'tencent',
secretId: '',
secretKey: ''
},
myLinux: {
providerType: 'SSH',
username: 'xxx',
password: 'xxx',
host: '1111.com',
port: 22,
publicKey: ''
}
},
cert: {
domains: ['*.docmirror.cn'],
email: 'xiaojunnuo@qq.com',
dnsProvider: 'aliyun',
csrInfo: {
country: 'CN',
state: 'GuangDong',
locality: 'ShengZhen',
organization: 'CertD Org.',
organizationUnit: 'IT Department',
emailAddress: 'xiaojunnuo@qq.com'
}
},
deploy: [
{
deployName: '流程1-部署到阿里云系列产品',
disabled: true,
tasks: [
{
taskName: '上传证书到云',
type: 'uploadCertToAliyun',
props:{
accessProvider: 'aliyun'
}
},
{ // CDN、SCDN、DCDN和负载均衡SLB
taskName: '部署证书到CDN',
type: 'deployCertToAliyunCDN',
props:{
domainName: 'certd-cdn-upload.docmirror.cn',
certName: 'certd部署测试(upload)',
certType: 'upload',
accessProvider: 'aliyun'
}
}
// {
// name: '部署证书到阿里云集群Ingress',
// type: 'deployCertToAliyunK8sIngress',
// accessProvider: 'aliyun'
// }
]
},
{
deployName: '流程2-部署到nginx服务器',
disabled: true,
tasks: [
{
taskName: '上传证书到服务器,并重启nginx',
type: 'sshAndExecute',
props:{
accessProvider: 'myLinux',
upload: [
{ from: '{certPath}', to: '/xxx/xxx/xxx.cert.pem' },
{ from: '{keyPath}', to: '/xxx/xxx/xxx.key' }
],
script: 'sudo systemctl restart nginx'
}
}
]
},
{
deployName: '流程3-触发jenkins任务',
disabled: true,
tasks: [
{
taskName: '触发jenkins任务',
type: 'sshAndExecute',
props:{
accessProvider: 'myLinux',
script: 'sudo systemctl restart nginx'
}
}
]
},
{
deployName: '流程4-部署到腾讯云ingress',
tasks: [
{
taskName: '上传到腾讯云',
type: 'uploadCertToTencent',
props:{
accessProvider: 'tencent-yonsz'
}
},
{
taskName: '部署到TKE-ingress',
type: 'deployCertToTencentTKEIngress',
props:{
clusterId:'cls-6lbj1vee',
ingressName:'ingress-base',
secretName:'certd-base-yonsz-net-hnuzjrzf',
accessProvider: 'tencent-yonsz'
}
}
]
}
]
}
_.merge(defaultOptions, optionsPrivate)
export function createOptions(){
return _.cloneDeep(defaultOptions)
}

5138
yarn.lock Normal file

File diff suppressed because it is too large Load Diff