feat: 上传证书到服务器,执行远程脚本

This commit is contained in:
xiaojunnuo
2021-01-03 02:30:34 +08:00
parent 67bff28255
commit 4cd7b02cb7
24 changed files with 552 additions and 123 deletions
@@ -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]
}
@@ -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)
@@ -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
}
@@ -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<void>}
*/
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)
@@ -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)
}
}
}
@@ -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 }) {
}
}
+110
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())
})
})
})
}
}
@@ -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 }) {
}
}
@@ -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)
@@ -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
@@ -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
}
@@ -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}`)
}
}
}
@@ -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