refactor: 1

This commit is contained in:
xiaojunnuo
2022-11-07 23:31:20 +08:00
parent f710c00c0d
commit d66bc33761
97 changed files with 1384 additions and 3562 deletions

View File

@@ -1,14 +1,21 @@
{
"extends": "standard",
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier"
],
"env": {
"mocha": true
},
"overrides": [
{
"files": ["*.test.js", "*.spec.js"],
"rules": {
"no-unused-expressions": "off"
}
}
]
"rules": {
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
}
}

View File

@@ -1,7 +1,26 @@
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
test/user.secret.ts

View File

@@ -1,31 +1,45 @@
{
"name": "@certd/plugin-host",
"version": "0.3.0",
"description": "",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/fast-crud.es.js",
"scripts": {
"build": "rollup -c"
},
"dependencies": {
"@certd/api": "^0.3.0",
"dayjs": "^1.9.7",
"lodash-es": "^4.17.20",
"ssh2": "^0.8.9"
},
"devDependencies": {
"@certd/certd": "^0.3.0",
"chai": "^4.2.0",
"eslint": "^7.15.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"mocha": "^8.2.1",
"rollup": "^3.2.3"
},
"author": "Greper",
"license": "MIT",
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
"name": "@certd/plugin-host",
"private": true,
"version": "0.3.0",
"main": "./src/index.ts",
"module": "./dist/plugin-aliyun.mjs",
"types": "./dist/es/plugin-aliyun.d.ts",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
"@certd/pipeline": "^0.3.0",
"ssh2": "^0.8.9"
},
"devDependencies": {
"log4js": "^6.3.0",
"dayjs": "^1.9.7",
"lodash-es": "^4.17.20",
"@types/lodash": "^4.14.186",
"vue-tsc": "^0.38.9",
"@alicloud/cs20151215": "^3.0.3",
"@alicloud/openapi-client": "^0.4.0",
"@alicloud/pop-core": "^1.7.10",
"@midwayjs/core": "^3.0.0",
"@midwayjs/decorator": "^3.0.0",
"@types/chai": "^4.3.3",
"@types/mocha": "^10.0.0",
"@types/node-forge": "^1.3.0",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"@typescript-eslint/parser": "^5.38.1",
"chai": "^4.3.6",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"log4js": "^6.3.0",
"mocha": "^10.1.0",
"ts-node": "^10.9.1",
"typescript": "^4.8.4",
"vite": "^3.1.0"
}
}

View File

@@ -1,13 +0,0 @@
export default {
input: 'src/index.js',
output: [
{
file: 'dist/index.cjs',
format: 'cjs'
},
{
file: 'dist/index.es.js',
format: 'es'
}
]
}

View File

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

View File

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

View File

@@ -0,0 +1,148 @@
import ssh2 from "ssh2";
import path from "path";
import _ from "lodash";
import { Logger } from "log4js";
export class SshClient {
logger: Logger;
constructor(logger: Logger) {
this.logger = logger;
}
/**
*
* @param connectConf
{
host: '192.168.100.100',
port: 22,
username: 'frylock',
password: 'nodejsrules'
}
* @param options
*/
uploadFiles(options: { connectConf: any; transports: any; sudo: boolean }) {
const { connectConf, transports, sudo } = options;
const conn = new ssh2.Client();
return new Promise((resolve, reject) => {
conn
.on("ready", () => {
this.logger.info("连接服务器成功");
conn.sftp(async (err: Error, sftp: any) => {
if (err) {
throw err;
}
try {
for (const transport of transports) {
this.logger.info("上传文件:", JSON.stringify(transport));
const sudoCmd = sudo ? "sudo" : "";
await this.exec({ connectConf, script: `${sudoCmd} mkdir -p ${path.dirname(transport.remotePath)} ` });
await this.fastPut({ sftp, ...transport });
}
resolve({});
} catch (e) {
reject(e);
} finally {
conn.end();
}
});
})
.connect(connectConf);
});
}
exec(options: { connectConf: any; script: string | Array<string> }) {
let { script } = options;
const { connectConf } = options;
if (_.isArray(script)) {
script = script.join("\n");
}
this.logger.info("执行命令:", script);
return new Promise((resolve, reject) => {
this.connect({
connectConf,
onReady: (conn: any) => {
conn.exec(script, (err: Error, stream: any) => {
if (err) {
reject(err);
return;
}
let data: any = null;
stream
.on("close", (code: any, signal: any) => {
this.logger.info(`[${connectConf.host}][close]:code:${code}`);
data = data ? data.toString() : null;
if (code === 0) {
resolve(data);
} else {
reject(new Error(data));
}
conn.end();
})
.on("data", (ret: any) => {
this.logger.info(`[${connectConf.host}][info]: ` + ret);
data = ret;
})
.stderr.on("data", (err: Error) => {
this.logger.info(`[${connectConf.host}][error]: ` + err);
data = err;
});
});
},
});
});
}
shell(options: { connectConf: any; script: string }) {
const { connectConf, script } = options;
return new Promise((resolve, reject) => {
this.connect({
connectConf,
onReady: (conn: any) => {
conn.shell((err: Error, stream: any) => {
if (err) {
reject(err);
return;
}
const output: any = [];
stream
.on("close", () => {
this.logger.info("Stream :: close");
conn.end();
resolve(output);
})
.on("data", (data: any) => {
this.logger.info("" + data);
output.push("" + data);
});
stream.end(script + "\nexit\n");
});
},
});
});
}
connect(options: { connectConf: any; onReady: any }) {
const { connectConf, onReady } = options;
const conn = new ssh2.Client();
conn
.on("ready", () => {
this.logger.info("Client :: ready");
onReady(conn);
})
.connect(connectConf);
return conn;
}
fastPut(options: { sftp: any; localPath: string; remotePath: string }) {
const { sftp, localPath, remotePath } = options;
return new Promise((resolve, reject) => {
sftp.fastPut(localPath, remotePath, (err: Error) => {
if (err) {
reject(err);
return;
}
resolve({});
});
});
}
}

View File

@@ -0,0 +1,54 @@
import { IsTask, TaskInput, TaskOutput, TaskPlugin, AbstractPlugin, RunStrategy } from "@certd/pipeline";
import { SshClient } from "../../lib/ssh";
@IsTask(() => {
return {
name: "hostShellExecute",
title: "执行远程主机脚本命令",
input: {
accessId: {
title: "主机登录配置",
helper: "登录",
component: {
name: "pi-access-selector",
type: "ssh",
},
required: true,
},
cert: {
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "pi-output-selector",
},
required: true,
},
script: {
title: "shell脚本命令",
component: {
name: "a-textarea",
vModel: "value",
},
},
},
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
output: {},
};
})
export class HostShellExecutePlugin extends AbstractPlugin implements TaskPlugin {
async execute(input: TaskInput): Promise<TaskOutput> {
const { script, accessId } = input;
const connectConf = this.accessService.getById(accessId);
const sshClient = new SshClient(this.logger);
const ret = await sshClient.exec({
connectConf,
script,
});
this.logger.info("exec res:", ret);
return {};
}
}

View File

@@ -0,0 +1,81 @@
import { IsTask, TaskInput, TaskOutput, TaskPlugin, AbstractPlugin, RunStrategy } from "@certd/pipeline";
import { SshClient } from "../../lib/ssh";
@IsTask(() => {
return {
name: "uploadCertToHost",
title: "上传证书到主机",
input: {
crtPath: {
title: "证书保存路径",
},
keyPath: {
title: "私钥保存路径",
},
cert: {
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "pi-output-selector",
},
required: true,
},
accessId: {
title: "主机登录配置",
helper: "access授权",
component: {
name: "pi-access-selector",
type: "ssh",
},
rules: [{ required: true, message: "此项必填" }],
},
sudo: {
title: "是否sudo",
component: {
name: "a-checkbox",
vModel: "checked",
},
},
},
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
output: {
hostCrtPath: {
title: "上传成功后的证书路径",
},
hostKeyPath: {
title: "上传成功后的私钥路径",
},
},
};
})
export class UploadCertToHostPlugin extends AbstractPlugin implements TaskPlugin {
async execute(input: TaskInput): Promise<TaskOutput> {
const { crtPath, keyPath, cert, accessId, sudo } = input;
const connectConf = this.accessService.getById(accessId);
const sshClient = new SshClient(this.logger);
await sshClient.uploadFiles({
connectConf,
transports: [
{
localPath: cert.crtPath,
remotePath: crtPath,
},
{
localPath: cert.keyPath,
remotePath: keyPath,
},
],
sudo,
});
this.logger.info("证书上传成功crtPath=", crtPath, ",keyPath=", keyPath);
return {
hostCrtPath: crtPath,
hostKeyPath: keyPath,
};
}
}

View File

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

View File

@@ -1,57 +0,0 @@
import { AbstractHostPlugin } from '../abstract-host.js'
import { SshClient } from '../ssh.js'
export class HostShellExecute extends AbstractHostPlugin {
/**
* 插件定义
* 名称
* 入参
* 出参
*/
static define () {
return {
name: 'hostShellExecute',
title: '执行远程主机脚本命令',
input: {
accessProvider: {
title: '主机登录配置',
helper: '登录',
component: {
name: 'access-selector',
type: 'ssh'
},
required: true
},
script: {
title: 'shell脚本命令',
component: {
name: 'a-textarea'
}
}
},
output: {
}
}
}
async execute ({ cert, props, context }) {
const { script, accessProvider } = props
const connectConf = this.getAccessProvider(accessProvider)
const sshClient = new SshClient()
const ret = await sshClient.exec({
connectConf,
script
})
return ret
}
/**
* @param cert
* @param props
* @param context
* @returns {Promise<void>}
*/
async rollback ({ cert, props, context }) {
}
}

View File

@@ -1,130 +0,0 @@
import ssh2 from 'ssh2'
import path from 'path'
import { util } from '@certd/api'
import _ from 'lodash'
const logger = util.logger
export class SshClient {
/**
*
* @param connectConf
{
host: '192.168.100.100',
port: 22,
username: 'frylock',
password: 'nodejsrules'
}
* @param transports
*/
uploadFiles ({ connectConf, transports, sudo = false }) {
const conn = new ssh2.Client()
return new Promise((resolve, reject) => {
conn.on('ready', () => {
logger.info('连接服务器成功')
conn.sftp(async (err, sftp) => {
if (err) {
throw err
}
try {
for (const transport of transports) {
logger.info('上传文件:', JSON.stringify(transport))
sudo = sudo ? 'sudo' : ''
await this.exec({ connectConf, script: `${sudo} mkdir -p ${path.dirname(transport.remotePath)} ` })
await this.fastPut({ sftp, ...transport })
}
resolve()
} catch (e) {
reject(e)
} finally {
conn.end()
}
})
}).connect(connectConf)
})
}
exec ({ connectConf, script }) {
if (_.isArray(script)) {
script = script.join('\n')
}
console.log('执行命令:', script)
return new Promise((resolve, reject) => {
this.connect({
connectConf,
onReady: (conn) => {
conn.exec(script, (err, stream) => {
if (err) {
reject(err)
return
}
let data = null
stream.on('close', (code, signal) => {
console.log(`[${connectConf.host}][close]:code:${code}`)
data = data ? data.toString() : null
if (code === 0) {
resolve(data)
} else {
reject(new Error(data))
}
conn.end()
}).on('data', (ret) => {
console.log(`[${connectConf.host}][info]: ` + ret)
data = ret
}).stderr.on('data', (err) => {
console.log(`[${connectConf.host}][error]: ` + err)
data = err
})
})
}
})
})
}
shell ({ connectConf, script }) {
return new Promise((resolve, reject) => {
this.connect({
connectConf,
onReady: (conn) => {
conn.shell((err, stream) => {
if (err) {
reject(err)
return
}
const output = []
stream.on('close', () => {
logger.info('Stream :: close')
conn.end()
resolve(output)
}).on('data', (data) => {
logger.info('' + data)
output.push('' + data)
})
stream.end(script + '\nexit\n')
})
}
})
})
}
connect ({ connectConf, onReady }) {
const conn = new ssh2.Client()
conn.on('ready', () => {
console.log('Client :: ready')
onReady(conn)
}).connect(connectConf)
return conn
}
fastPut ({ sftp, localPath, remotePath }) {
return new Promise((resolve, reject) => {
sftp.fastPut(localPath, remotePath, (err) => {
if (err) {
reject(err)
return
}
resolve()
})
})
}
}

View File

@@ -1,85 +0,0 @@
import { AbstractHostPlugin } from '../abstract-host.js'
import { SshClient } from '../ssh.js'
export class UploadCertToHost extends AbstractHostPlugin {
/**
* 插件定义
* 名称
* 入参
* 出参
*/
static define () {
return {
name: 'uploadCertToHost',
title: '上传证书到主机',
input: {
crtPath: {
title: '证书保存路径'
},
keyPath: {
title: '私钥保存路径'
},
accessProvider: {
title: '主机登录配置',
helper: 'access授权',
component: {
name: 'access-selector',
type: 'ssh'
},
rules: [{ required: true, message: '此项必填' }]
},
sudo: {
title: '是否sudo',
component: {
name: 'a-checkbox',
vModel: 'checked'
}
}
},
output: {
hostCrtPath: {
helper: '上传成功后的证书路径'
},
hostKeyPath: {
helper: '上传成功后的私钥路径'
}
}
}
}
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

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

View File

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

View File

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