前言
最近忙于换公司,所以笔记好长时间没写,光忙着去刷leetcode算法和面试题了,现在工作正式换好了,后面有时间可以把新工作中遇到的相关解决思路记录下来了。
刚来第一天,熟悉工程阶段,发现新工作负责的前端项目,整个构建发布流程过于繁复,大概需要:本地构建 -> 登录到服务器 -> 服务器上新建新的目录 -> 将本地构建传到此目录下 -> 构建docker镜像 -> 给该镜像打tag -> 推送到镜像服务器 -> Rancher上修改tag号重新部署服务
整个过程其实是缺少了jekins或者bamboo之类的自动构建部署工具,所以整个流程需要手动执行,问题主要是步骤繁琐、命令复杂、版本号命名、docker镜像构建推送复杂等。
据我来的这两天观察来看,dev环境平均每天都要发版1~3次左右,uat环境每周都会有发版,也就是说发布相对频繁,重复性工作过多,极大地浪费了开发人员的时间,所以决定用Node.js将重复性工作接管,减少开发人员的机械性工作。
思路
首先,明确服务器上不能搭建jekins或其他自动化构建部署工具(服务器资源紧张,没必要因为单独一个项目搭建自动化工具)。
本地构建npm run build
以及最后一步Rancher部署工作,属于非关联流程(最后一步其实可以通过webhooks也加入流程,但是在未想好回退机制前,此步骤先不纳入流程中)。
主要流程就是:登录到服务器 -> 服务器上新建新的目录 -> 将本地构建传到此目录下 -> 构建docker镜像 -> 给该镜像打tag -> 推送到镜像服务器
通过这个步骤,可以得知,程序至少要实现:获取用户输入
、ssh连接
、执行shell命令
、ftp上传
等功能。
开发
日志输出:
chalk
通过引入chalk来做到日志颜色的输出来区分错误、成功以及提示性消息。
使用方式:
1
2
3console.log(chalk.red.bold('错误日志'));
console.log(chalk.green.bold('成功日志'));
console.log(chalk.blue.bold('提示日志'));获取用户命令行输入:
readline
使用readline来捕获命令行输入,封装一个方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
* 同步获取问题
* @param {*} question
* @returns
*/
function readSyncByRl(question) {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question(question || '>', (answer) => {
rl.close();
resolve(answer.trim());
});
});
}获取用户输入:
1
2
3
4const host = await readSyncByRl('请输入服务器地址:');
const port = await readSyncByRl('请输入服务器端口号:');
const username = await readSyncByRl('请输入服务器账号:');
const password = await readSyncByRl('请输入服务器密码:');执行shell命令
ssh2
使用ssh2来连接目标服务器,同时执行shell命令
封装方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45/**
* 连接服务器
* @param {host, port, username, password} server
* @param {*} then
*/
function doConnect(server, then) {
const conn = new Client();
conn.on('ready', () => {
console.log(chalk.green.bold(`服务器[${server.host}]连接成功!`));
then && then(conn);
}).on('error', (err) => {
console.error(chalk.red.bold(`服务器[${server.host}]连接失败!`), err)
}).on('close', () => {
conn.end()
}).connect(server);
}
/**
* 执行shell命令
* @param {host, port, username, password} server
* @param {*} cmd
* @param {*} then
*/
function doShell(server, cmd, then) {
doConnect(server, (conn) => {
conn.shell((err, stream) => {
if (err) throw err;
let res = '';
stream.on('close', () => {
conn.end();
then && then(res);
}).on('data', (data) => {
res += data;
console.log(res);
}).stderr.on('data', (data) => {
console.log(chalk.red.bold(`执行Shell发生错误:${data}`))
});
if (cmd instanceof Array) {
stream.end(`${cmd.join(' && ')}\nexit\n`);
} else {
stream.end(`${cmd}\nexit\n`);
}
});
})
}使用:
1
doShell({ host, port, username, password }, `mkdir -p ${remotePath}`, () => console.log(`新建tag目录${remotePath}成功`));
使用sftp上传文件:
ssh2-sftp-client
这里需要说明的是,ssh2也能上传文件,但是它不支持上传文件夹,只能上传文件,因此,如果不使用该npm库de话,需要手动实现文件夹的迭代循环,以及调用新建文件夹shell命令,过程相对繁琐,所以引入了一个新的npm库来处理上传
封装方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* 上传
* @param {*} localPath
* @param {*} remotePath
* @param {*} then
*/
async function doUpload(localPath, remotePath) {
const tempPath = path.resolve(__dirname, `../${localPath}`);
try {
const stat = fs.statSync(tempPath);
if (stat.isFile()) {
console.log(`上传文件${localPath}...`);
return sftp.put(tempPath, `${remotePath}/${localPath}`);
}
if (stat.isDirectory()) {
console.log(`上传文件夹${localPath}...`);
return sftp.uploadDir(tempPath, `${remotePath}/${localPath}`);
}
} catch(err) {
console.log(chalk.red.bold(`【${localPath}】上传失败, 原因:`, err));
process.exit(0);
}
}使用方法如下:
1
2
3
4await sftp.connect(server);
await doUpload('dist', remotePath);
console.log(chalk.green.bold('文件上传成功'));
sftp.end();
其他
更改文件格式
其中上传的文件中,有一个shell脚本,此脚本上传后,需要修改其文件格式,否则执行会失败,更改方式如下:
1
sed -i 's/\r$//g' docker/setenv.sh
- 密码输入
执行sudo命令或者docker login的时候,都需要输入密码,但是在脚本中,显然没办法捕获输入,可以使用管道符来完成:
1
2echo ${password} | sudo -S docker build -t ${preTag}:v${tag} .
echo ${dockerpassword}|sudo docker login --username ${dockerusername} --password-stdin ${docker}
完整代码
因为涉及到了公司项目,所以完整代码不放出来了,核心逻辑及难点已经写完,剩下的,都是边边角角,很容易了