From 4cd7b02cb729380dc537e01bb14fa78c54725ab0 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sun, 3 Jan 2021 02:30:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=8A=E4=BC=A0=E8=AF=81=E4=B9=A6?= =?UTF-8?q?=E5=88=B0=E6=9C=8D=E5=8A=A1=E5=99=A8=EF=BC=8C=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/certd/src/store/cert-store.js | 18 ++- packages/executor/src/index.js | 90 +++++++++----- packages/executor/src/trace.js | 76 ++++++++++++ packages/executor/test/index.test.js | 5 +- packages/plugins/package.json | 1 + packages/plugins/src/abstract-plugin/index.js | 5 +- .../plugins/src/aliyun/abstract-aliyun.js | 7 -- .../plugins/src/aliyun/deploy-to-cdn/index.js | 8 +- .../src/aliyun/upload-to-aliyun/index.js | 13 +-- packages/plugins/src/host/abstract-host.js | 9 ++ .../src/host/host-shell-execute/index.js | 51 ++++++++ packages/plugins/src/host/ssh.js | 110 ++++++++++++++++++ .../plugins/src/host/upload-to-host/index.js | 77 ++++++++++++ .../plugins/src/tencent/abstract-tencent.js | 7 -- .../src/tencent/deploy-to-cdn/index.js | 10 +- .../src/tencent/deploy-to-clb/index.js | 10 +- .../tencent/deploy-to-tke-ingress/index.js | 68 +++++------ .../src/tencent/upload-to-tencent/index.js | 12 +- .../test/host/host-shell-execute.test.js | 29 +++++ .../plugins/test/host/upload-to-host.test.js | 27 +++++ .../test/tencent/deploy-to-cdn.test.js | 4 +- .../test/tencent/deploy-to-clb.test.js | 9 +- .../test/tencent/upload-to-tencent.test.js | 4 +- packages/plugins/yarn.lock | 25 +++- 24 files changed, 552 insertions(+), 123 deletions(-) create mode 100644 packages/executor/src/trace.js create mode 100644 packages/plugins/src/host/abstract-host.js create mode 100644 packages/plugins/src/host/host-shell-execute/index.js create mode 100644 packages/plugins/src/host/ssh.js create mode 100644 packages/plugins/src/host/upload-to-host/index.js create mode 100644 packages/plugins/test/host/host-shell-execute.test.js create mode 100644 packages/plugins/test/host/upload-to-host.test.js diff --git a/packages/certd/src/store/cert-store.js b/packages/certd/src/store/cert-store.js index c153e1b8..d4ab76d9 100644 --- a/packages/certd/src/store/cert-store.js +++ b/packages/certd/src/store/cert-store.js @@ -26,14 +26,21 @@ export class CertStore { 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, cert.crt) - await this.store.set(priKey, cert.key) + await this.store.set(crtKey, this.formatCert(cert.crt)) + await this.store.set(priKey, this.formatCert(cert.key)) await this.store.set(csrKey, cert.csr) await this.store.link(newDir, this.currentRootPath) @@ -53,7 +60,12 @@ export class CertStore { const csr = await this.store.get(csrKey) return { - crt, key, csr, certDir: this.store.getActualKey(dir) + crt: this.formatCert(crt), + key: this.formatCert(key), + csr, + crtPath: this.store.getActualKey(crtKey), + keyPath: this.store.getActualKey(priKey), + certDir: this.store.getActualKey(dir) } } diff --git a/packages/executor/src/index.js b/packages/executor/src/index.js index 51d779d8..c119aa9b 100644 --- a/packages/executor/src/index.js +++ b/packages/executor/src/index.js @@ -3,12 +3,14 @@ import DefaultPlugins from '@certd/plugins' import logger from './util.log.js' import _ from 'lodash' import dayjs from 'dayjs' +import { Trace } from './trace.js' export class Executor { constructor (args = {}) { const { plugins } = args this.plugins = {} this.usePlugins(DefaultPlugins) this.usePlugins(plugins) + this.trace = new Trace() } use (plugin) { @@ -32,17 +34,18 @@ export class Executor { async run (options, args) { try { - return await this.doRun(options, args) + if (args != null) { + _.merge(options.args, args) + } + return await this.doRun(options) } catch (e) { logger.error('任务执行出错:', e) throw e } } - async doRun (options, args) { - if (args != null) { - _.merge(options.args, args) - } + async doRun (options) { + // 申请证书 logger.info('任务开始') const certd = new Certd(options) const cert = await this.runCertd(certd) @@ -50,28 +53,36 @@ export class Executor { throw new Error('申请证书失败') } logger.info('证书保存路径:', cert.certDir) + + logger.info('----------------------') if (!cert.isNew) { // 如果没有更新 - if (!options.args.forceDeploy) { + if (!options.args?.forceDeploy && !options.args?.forceRedeploy) { // 且不需要强制运行deploy logger.info('证书无更新,无需重新部署') logger.info('任务完成') return { cert } } } - - let context = {} + // 读取上次执行进度 + let context = { + certIsNew: !!cert.isNew + } const contextJson = await certd.certStore.getCurrentFile('context.json') if (contextJson) { context = JSON.parse(contextJson) } + + const trace = new Trace(context) + // 运行部署任务 try { - await this.runDeploys({ options, cert, context }) + await this.runDeploys({ options, cert, context, trace }) } finally { await certd.certStore.setCurrentFile('context.json', JSON.stringify(context)) } logger.info('任务完成') + trace.print() return { cert, context @@ -79,54 +90,69 @@ export class Executor { } async runCertd (certd) { - logger.info(`申请证书${JSON.stringify(certd.options.cert.domains)}开始`) + logger.info(`证书任务 ${JSON.stringify(certd.options.cert.domains)} 开始`) const cert = await certd.certApply() - logger.info(`申请证书${JSON.stringify(certd.options.cert.domains)}完成`) + logger.info(`证书任务 ${JSON.stringify(certd.options.cert.domains)} 完成`) return cert } - async runDeploys ({ options, cert, context }) { + 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) { - logger.info(`--部署任务【${deploy.deployName}】开始`) + const deployName = deploy.deployName + logger.info(`------------【${deployName}】-----------`) if (deploy.disabled === true) { - logger.info('----此部署任务已被禁用,跳过') + logger.info('此流程已被禁用,跳过') + logger.info('') + trace.set({ deployName, value: { current: 'skip', status: 'disabled', remark: '流程禁用' } }) continue } - for (const task of deploy.tasks) { - await this.runTask({ options, cert, task, context }) + 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 } }) + logger.error('流程执行失败', e) } - logger.info(`--部署任务【${deploy.deployName}】完成`) + + logger.info('') } } - async runTask ({ options, task, cert, context }) { + 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}还未安装`) + throw new Error(`插件:${taskType}还未安装`) } - logger.info(`----任务【${task.taskName}】开始执行`) let instance = Plugin if (Plugin instanceof Function) { - instance = new Plugin() + instance = new Plugin({ accessProviders: options.accessProviders }) } - if (context.progress && context.progress[task.taskName] && context.progress[task.taskName].success) { - logger.info(`----任务【${task.taskName}】已经执行完成,跳过此任务`) + 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 } - await instance.execute({ cert, accessProviders: options.accessProviders, props: task.props, context }) - if (context.progress == null) { - context.progress = {} - } - context.progress[task.taskName] = { - success: true, - time: dayjs().format() - } - logger.info(`----任务【${task.taskName}】执行完成`) + logger.info(`----【${taskName}】开始执行`) + await instance.execute({ cert, props: task.props, context }) + trace.set({ deployName, taskName, value: { current: 'success', status: 'success', remark: '执行成功', time: dayjs().format() } }) + logger.info(`----任务【${taskName}】执行完成`) + logger.info('') } } diff --git a/packages/executor/src/trace.js b/packages/executor/src/trace.js new file mode 100644 index 00000000..a0334fb0 --- /dev/null +++ b/packages/executor/src/trace.js @@ -0,0 +1,76 @@ +import logger from './util.log.js' +import _ from 'lodash' +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: 'skip', remark: '还未到过期时间,跳过' }, '更新证书') + } else { + this.printTraceLine({ current: 'success', 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, ' └') + } + } + } + } + + 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 + } +} diff --git a/packages/executor/test/index.test.js b/packages/executor/test/index.test.js index 9307f04e..683df735 100644 --- a/packages/executor/test/index.test.js +++ b/packages/executor/test/index.test.js @@ -5,6 +5,7 @@ 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) @@ -12,6 +13,7 @@ describe('AutoDeploy', function () { 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 }) @@ -19,9 +21,10 @@ describe('AutoDeploy', function () { 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 }) + const ret = await executor.run(options, { forceCert: false, forceDeploy: true, forceRedeploy: true }) expect(ret).ok expect(ret.cert).ok }) diff --git a/packages/plugins/package.json b/packages/plugins/package.json index fe75aeb4..ecc03ce3 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -15,6 +15,7 @@ "kubernetes-client": "^9.0.0", "lodash": "^4.17.20", "log4js": "^6.3.0", + "ssh2": "^0.8.9", "tencentcloud-sdk-nodejs": "^4.0.39" }, "devDependencies": { diff --git a/packages/plugins/src/abstract-plugin/index.js b/packages/plugins/src/abstract-plugin/index.js index e61b7fd7..45a0e68f 100644 --- a/packages/plugins/src/abstract-plugin/index.js +++ b/packages/plugins/src/abstract-plugin/index.js @@ -3,8 +3,9 @@ import logger from '../utils/util.log.js' import dayjs from 'dayjs' import Sleep from '../utils/util.sleep.js' export class AbstractPlugin { - constructor () { + constructor ({ accessProviders }) { this.logger = logger + this.accessProviders = accessProviders } appendTimeSuffix (name) { @@ -58,7 +59,7 @@ export class AbstractPlugin { console.error('请实现此方法,rollback:', options.context) } - getAccessProvider (accessProvider, accessProviders) { + getAccessProvider (accessProvider, accessProviders = this.accessProviders) { if (typeof accessProvider === 'string' && accessProviders) { accessProvider = accessProviders[accessProvider] } diff --git a/packages/plugins/src/aliyun/abstract-aliyun.js b/packages/plugins/src/aliyun/abstract-aliyun.js index 89be83fc..3c10c01e 100644 --- a/packages/plugins/src/aliyun/abstract-aliyun.js +++ b/packages/plugins/src/aliyun/abstract-aliyun.js @@ -1,13 +1,6 @@ import { AbstractPlugin } from '../abstract-plugin/index.js' export class AbstractAliyunPlugin extends AbstractPlugin { - format (pem) { - pem = pem.replace(/\r/g, '') - pem = pem.replace(/\n\n/g, '\n') - pem = pem.replace(/\n$/g, '') - return pem - } - checkRet (ret) { if (ret.code != null) { throw new Error('执行失败:', ret.Message) diff --git a/packages/plugins/src/aliyun/deploy-to-cdn/index.js b/packages/plugins/src/aliyun/deploy-to-cdn/index.js index fa912dba..d14a80fb 100644 --- a/packages/plugins/src/aliyun/deploy-to-cdn/index.js +++ b/packages/plugins/src/aliyun/deploy-to-cdn/index.js @@ -51,8 +51,8 @@ export class DeployCertToAliyunCDN extends AbstractAliyunPlugin { } } - async execute ({ accessProviders, cert, props, context }) { - const accessProvider = this.getAccessProvider(props.accessProvider, accessProviders) + 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) @@ -77,8 +77,8 @@ export class DeployCertToAliyunCDN extends AbstractAliyunPlugin { ServerCertificateStatus: 'on', CertName: CertName, CertType: from, - ServerCertificate: super.format(cert.crt.toString()), - PrivateKey: super.format(cert.key.toString()) + ServerCertificate: cert.crt, + PrivateKey: cert.key } return params } diff --git a/packages/plugins/src/aliyun/upload-to-aliyun/index.js b/packages/plugins/src/aliyun/upload-to-aliyun/index.js index 085d5083..557170cc 100644 --- a/packages/plugins/src/aliyun/upload-to-aliyun/index.js +++ b/packages/plugins/src/aliyun/upload-to-aliyun/index.js @@ -1,5 +1,4 @@ import Core from '@alicloud/pop-core' -import dayjs from 'dayjs' import { AbstractAliyunPlugin } from '../abstract-aliyun.js' export class UploadCertToAliyun extends AbstractAliyunPlugin { /** @@ -45,21 +44,21 @@ export class UploadCertToAliyun extends AbstractAliyunPlugin { }) } - async execute ({ accessProviders, cert, props, context }) { + 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: this.format(cert.crt.toString()), - Key: this.format(cert.key.toString()) + Cert: cert.crt, + Key: cert.key } const requestOption = { method: 'POST' } - const provider = super.getAccessProvider(accessProvider, accessProviders) + const provider = this.getAccessProvider(accessProvider) const client = this.getClient(provider) const ret = await client.request('CreateUserCertificate', params, requestOption) this.checkRet(ret) @@ -75,7 +74,7 @@ export class UploadCertToAliyun extends AbstractAliyunPlugin { * @param context * @returns {Promise} */ - async rollback ({ accessProviders, cert, props, context }) { + async rollback ({ cert, props, context }) { const { accessProvider } = props const { aliyunCertId } = context this.logger.info('准备删除阿里云证书:', aliyunCertId) @@ -88,7 +87,7 @@ export class UploadCertToAliyun extends AbstractAliyunPlugin { method: 'POST' } - const provider = super.getAccessProvider(accessProvider, accessProviders) + const provider = this.getAccessProvider(accessProvider) const client = this.getClient(provider) const ret = await client.request('DeleteUserCertificate', params, requestOption) this.checkRet(ret) diff --git a/packages/plugins/src/host/abstract-host.js b/packages/plugins/src/host/abstract-host.js new file mode 100644 index 00000000..f2e1d9ca --- /dev/null +++ b/packages/plugins/src/host/abstract-host.js @@ -0,0 +1,9 @@ +import { AbstractPlugin } from '../abstract-plugin/index.js' + +export class AbstractHostPlugin extends AbstractPlugin { + checkRet (ret) { + if (ret.code != null) { + throw new Error('执行失败:', ret.Message) + } + } +} diff --git a/packages/plugins/src/host/host-shell-execute/index.js b/packages/plugins/src/host/host-shell-execute/index.js new file mode 100644 index 00000000..9582fab1 --- /dev/null +++ b/packages/plugins/src/host/host-shell-execute/index.js @@ -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} + */ + async rollback ({ cert, props, context }) { + + } +} diff --git a/packages/plugins/src/host/ssh.js b/packages/plugins/src/host/ssh.js new file mode 100644 index 00000000..6def5e52 --- /dev/null +++ b/packages/plugins/src/host/ssh.js @@ -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()) + }) + }) + }) + } +} diff --git a/packages/plugins/src/host/upload-to-host/index.js b/packages/plugins/src/host/upload-to-host/index.js new file mode 100644 index 00000000..762ca244 --- /dev/null +++ b/packages/plugins/src/host/upload-to-host/index.js @@ -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} + */ + async rollback ({ cert, props, context }) { + + } +} diff --git a/packages/plugins/src/tencent/abstract-tencent.js b/packages/plugins/src/tencent/abstract-tencent.js index 06c770fd..4ea9a77a 100644 --- a/packages/plugins/src/tencent/abstract-tencent.js +++ b/packages/plugins/src/tencent/abstract-tencent.js @@ -1,13 +1,6 @@ import { AbstractPlugin } from '../abstract-plugin/index.js' export class AbstractTencentPlugin extends AbstractPlugin { - format (pem) { - pem = pem.replace(/\r/g, '') - pem = pem.replace(/\n\n/g, '\n') - pem = pem.replace(/\n$/g, '') - return pem - } - checkRet (ret) { if (!ret || ret.Error) { throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message) diff --git a/packages/plugins/src/tencent/deploy-to-cdn/index.js b/packages/plugins/src/tencent/deploy-to-cdn/index.js index 69419811..9db042ce 100644 --- a/packages/plugins/src/tencent/deploy-to-cdn/index.js +++ b/packages/plugins/src/tencent/deploy-to-cdn/index.js @@ -52,14 +52,14 @@ export class DeployCertToTencentCDN extends AbstractTencentPlugin { } } - async execute ({ accessProviders, cert, props, context }) { - const accessProvider = this.getAccessProvider(props.accessProvider, accessProviders) + 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 ({ accessProviders, cert, props, context }) { + async rollback ({ cert, props, context }) { } @@ -99,8 +99,8 @@ export class DeployCertToTencentCDN extends AbstractTencentPlugin { } if (from === 'upload' || tencentCertId == null) { params.Https.CertInfo = { - Certificate: this.format(cert.crt.toString()), - PrivateKey: this.format(cert.key.toString()) + Certificate: cert.crt, + PrivateKey: cert.key } } return params diff --git a/packages/plugins/src/tencent/deploy-to-clb/index.js b/packages/plugins/src/tencent/deploy-to-clb/index.js index b2e31c10..cb42a84a 100644 --- a/packages/plugins/src/tencent/deploy-to-clb/index.js +++ b/packages/plugins/src/tencent/deploy-to-clb/index.js @@ -48,8 +48,8 @@ export class DeployCertToTencentCLB extends AbstractTencentPlugin { } } - async execute ({ accessProviders, cert, props, context }) { - const accessProvider = this.getAccessProvider(props.accessProvider, accessProviders) + async execute ({ cert, props, context }) { + const accessProvider = this.getAccessProvider(props.accessProvider) const { region } = props const client = this.getClient(accessProvider, region) @@ -104,7 +104,7 @@ export class DeployCertToTencentCLB extends AbstractTencentPlugin { return certId } - async rollback ({ accessProviders, cert, props, context }) { + async rollback ({ cert, props, context }) { this.logger.warn('未实现rollback') } @@ -140,8 +140,8 @@ export class DeployCertToTencentCLB extends AbstractTencentPlugin { if (tencentCertId == null) { params.Certificate.CertName = this.appendTimeSuffix(certName || cert.domain) - params.Certificate.CertKey = this.format(cert.key.toString()) - params.Certificate.CertContent = this.format(cert.crt.toString()) + params.Certificate.CertKey = cert.key + params.Certificate.CertContent = cert.crt } return params } diff --git a/packages/plugins/src/tencent/deploy-to-tke-ingress/index.js b/packages/plugins/src/tencent/deploy-to-tke-ingress/index.js index 99520912..9ad64dcf 100644 --- a/packages/plugins/src/tencent/deploy-to-tke-ingress/index.js +++ b/packages/plugins/src/tencent/deploy-to-tke-ingress/index.js @@ -14,20 +14,28 @@ export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin { name: 'deployCertToTencentTKEIngress', label: '部署到腾讯云TKE-ingress', input: { + region: { + label: '大区', + value: 'ap-guangzhou' + }, clusterId: { label: '集群ID', required: true, desc: '例如:cls-6lbj1vee' }, - region: { - label: '大区', - value: 'ap-guangzhou' + namespace: { + label: '集群的namespace', + value: 'default' }, secreteName: { - label: '证书的secret名称' + type: [String, Array], + label: '证书的secret名称', + desc: '支持多个(传入数组)' }, ingressName: { - label: 'ingress名称' + type: [String, Array], + label: 'ingress名称', + desc: '支持多个(传入数组)' }, accessProvider: { label: 'Access提供者', @@ -43,11 +51,12 @@ export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin { } } - async execute ({ accessProviders, cert, props, context }) { - const accessProvider = this.getAccessProvider(props.accessProvider, accessProviders) + 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) await this.patchCertSecret({ k8sClient, props, context }) await this.sleep(2000) // 停留2秒,等待secret部署完成 @@ -84,33 +93,6 @@ export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin { return ret.Kubeconfig } - async createCertSecret ({ k8sClient, props, cert, context }) { - const { tencentCertId } = context - if (tencentCertId == null) { - throw new Error('请先将【上传证书到腾讯云】作为前置任务') - } - const certIdBase64 = Buffer.from(tencentCertId).toString('base64') - - let name = 'cert-' + cert.domain.replace(/\./g, '-') - name = name.replace(/\*/g, '-') - // name = this.appendTimeSuffix(name) - - const body = { - kind: 'Secret', - data: { - qcloud_cert_id: certIdBase64 - }, - metadata: { - name, - labels: { - certd: 'certd-' + this.getSafetyDomain(cert.domain) - } - }, - type: 'Opaque' - } - return await k8sClient.createSecret({ namespace: props.namespace, body: body }) - } - async patchCertSecret ({ k8sClient, props, context }) { const { tencentCertId } = context if (tencentCertId == null) { @@ -130,7 +112,14 @@ export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin { } } } - return await k8sClient.patchSecret({ namespace, secretName, body }) + 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 }) { @@ -143,6 +132,13 @@ export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin { } } } - return await k8sClient.patchIngress({ namespace, ingressName, body }) + 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}`) + } } } diff --git a/packages/plugins/src/tencent/upload-to-tencent/index.js b/packages/plugins/src/tencent/upload-to-tencent/index.js index a456e2b3..f52632f3 100644 --- a/packages/plugins/src/tencent/upload-to-tencent/index.js +++ b/packages/plugins/src/tencent/upload-to-tencent/index.js @@ -52,16 +52,16 @@ export class UploadCertToTencent extends AbstractTencentPlugin { return new SslClient(clientConfig) } - async execute ({ accessProviders, cert, props, context, logger }) { + async execute ({ cert, props, context, logger }) { const { name, accessProvider } = props const certName = this.appendTimeSuffix(name) - const provider = super.getAccessProvider(accessProvider, accessProviders) + const provider = this.getAccessProvider(accessProvider) const client = this.getClient(provider) const params = { - CertificatePublicKey: this.format(cert.crt.toString()), - CertificatePrivateKey: this.format(cert.key.toString()), + CertificatePublicKey: cert.crt, + CertificatePrivateKey: cert.key, Alias: certName } const ret = await client.UploadCertificate(params) @@ -70,9 +70,9 @@ export class UploadCertToTencent extends AbstractTencentPlugin { context.tencentCertId = ret.CertificateId } - async rollback ({ accessProviders, cert, props, context }) { + async rollback ({ cert, props, context }) { const { accessProvider } = props - const provider = super.getAccessProvider(accessProvider, accessProviders) + const provider = super.getAccessProvider(accessProvider) const client = this.getClient(provider) const { tencentCertId } = context diff --git a/packages/plugins/test/host/host-shell-execute.test.js b/packages/plugins/test/host/host-shell-execute.test.js new file mode 100644 index 00000000..c5b7630e --- /dev/null +++ b/packages/plugins/test/host/host-shell-execute.test.js @@ -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) + }) +}) diff --git a/packages/plugins/test/host/upload-to-host.test.js b/packages/plugins/test/host/upload-to-host.test.js new file mode 100644 index 00000000..df98e6fe --- /dev/null +++ b/packages/plugins/test/host/upload-to-host.test.js @@ -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) + }) +}) diff --git a/packages/plugins/test/tencent/deploy-to-cdn.test.js b/packages/plugins/test/tencent/deploy-to-cdn.test.js index cc919c0d..5965e004 100644 --- a/packages/plugins/test/tencent/deploy-to-cdn.test.js +++ b/packages/plugins/test/tencent/deploy-to-cdn.test.js @@ -36,9 +36,11 @@ describe('DeployToTencentCDN', function () { 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 = certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn']) + const cert = await certd.readCurrentCert() const context = {} const deployOpts = { accessProviders: options.accessProviders, diff --git a/packages/plugins/test/tencent/deploy-to-clb.test.js b/packages/plugins/test/tencent/deploy-to-clb.test.js index 2a2aff13..9af8d572 100644 --- a/packages/plugins/test/tencent/deploy-to-clb.test.js +++ b/packages/plugins/test/tencent/deploy-to-clb.test.js @@ -29,15 +29,14 @@ describe('DeployToTencentCLB', 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', loadBalancerId: 'lb-59yhe5xo' } - 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) @@ -51,8 +50,10 @@ describe('DeployToTencentCLB', function () { 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 = certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn']) + const cert = await certd.readCurrentCert() const deployPlugin = new DeployCertToTencentCLB() const context = {} const deployOpts = { diff --git a/packages/plugins/test/tencent/upload-to-tencent.test.js b/packages/plugins/test/tencent/upload-to-tencent.test.js index 6326388d..1396a556 100644 --- a/packages/plugins/test/tencent/upload-to-tencent.test.js +++ b/packages/plugins/test/tencent/upload-to-tencent.test.js @@ -8,8 +8,10 @@ describe('PluginUploadToTencent', 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 = certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn']) + const cert = await certd.readCurrentCert() const context = {} const uploadOpts = { accessProviders: options.accessProviders, diff --git a/packages/plugins/yarn.lock b/packages/plugins/yarn.lock index 1407a63f..05dffe81 100644 --- a/packages/plugins/yarn.lock +++ b/packages/plugins/yarn.lock @@ -382,7 +382,7 @@ array.prototype.flatmap@^1.2.3: es-abstract "^1.18.0-next.1" function-bind "^1.1.1" -asn1@~0.2.3: +asn1@~0.2.0, asn1@~0.2.3: version "0.2.4" resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== @@ -458,7 +458,7 @@ base64url@^3.0.1: resolved "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== -bcrypt-pbkdf@^1.0.0: +bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= @@ -2500,6 +2500,22 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +ssh2-streams@~0.4.10: + version "0.4.10" + resolved "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34" + integrity sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ== + dependencies: + asn1 "~0.2.0" + bcrypt-pbkdf "^1.0.2" + streamsearch "~0.1.2" + +ssh2@^0.8.9: + version "0.8.9" + resolved "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz#54da3a6c4ba3daf0d8477a538a481326091815f3" + integrity sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw== + dependencies: + ssh2-streams "~0.4.10" + sshpk@^1.7.0: version "1.16.1" resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -2524,6 +2540,11 @@ streamroller@^2.2.4: debug "^4.1.1" fs-extra "^8.1.0" +streamsearch@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + "string-width@^1.0.2 || 2": version "2.1.1" resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"