您现在的位置是:首页 >技术杂谈 >测试环境一键发布网站首页技术杂谈
测试环境一键发布
简介测试环境一键发布
背景
目前公司项目发布测试环境不够自动化,每次需要手动打包并且手动更新,影响开发效率
流程图
而且因为是本地手动发布,容易失误造成一些不必要的麻烦:
-
远端代码有更新,忘记拉取代码
-
快速发布,本地代码忘了提交,导致远端代码缺失
-
误将本地的一些测试代码发布
...
因此,如果能够将操作脚本化,不仅可以提高开发效率,同时也能减少失误造成的影响。
一键发布
使用
-
先将deploy.js拷贝到需要发布项目的根目录
const { execSync } = require('child_process')
const fs = require('fs');
const readline = require('readline');
const packageJson = require('./package.json')
const Axios = require('axios').default;
const config = {
container: 'simulated-trade-h5-test-container', // 容器名称
buildScript: process.env.DEPLOY_ENV == 'prod' ? 'npm run build:pro' : 'npm run build:test', // 打包命令
apiUrl: 'http://10.156.160.11:9000/api', // portainer接口地址
env: process.env.DEPLOY_ENV || 'test', // ['local', 'test', 'prod'] 设置为local可通过本地代码进行部署,
}
const axios = Axios.create({
baseURL: config.apiUrl,
})
// 远程仓库地址
const url = execSync('git config --get remote.origin.url')
const dirname = __dirname.split(/[\/]/)
// 当前文件夹名称
const folder = dirname.slice(-1).toString()
// 当前分支
const branch = execSync("git branch --show-current")
// 创建副本仓库
process.chdir('../')
if (!fs.existsSync(`.deploy`)) {
fs.mkdirSync('.deploy')
}
process.chdir('./.deploy')
if (!fs.existsSync(folder)) {
execSync(`git clone ${url} ${folder}`, {stdio: 'inherit'})
}
// 创建缓存文件
if (!fs.existsSync(`.cache`)) {
fs.writeFileSync('.cache', '')
}
const cacheFile = fs.readFileSync('.cache').toString()
const cache = {}
cacheFile.split('
').forEach(i => {
const arr = i.split('=')
cache[arr[0]] = arr[1]
})
if (config.env == 'local') {
console.log('本地仓库环境');
process.chdir(`../${folder}`)
} else {
console.log('远程仓库环境');
process.chdir(folder)
execSync(`git fetch --all && git reset --hard origin/${branch}`, {stdio: 'inherit'})
console.log('安装依赖...');
execSync('npm i', {stdio: 'inherit'})
}
axios.interceptors.request.use(config => {
if (cache.token) {
config.headers.Authorization = `Bearer ${cache.token}`
}
return config
})
// 检查token
const checkToken = async () => {
const token = cache.token
if (!token) {
return false
}
try {
await axios.get('/endpoints/3/docker/containers/json?all=1')
} catch {
return false
}
return true
}
// 发布
const deploy = async () => {
try {
const hasToken = await checkToken()
if (!hasToken) {
// 读取用户输入
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const q = (str) => new Promise(resolve => {
rl.question(str, res => {
resolve(res)
})
})
let username = ''
let password = ''
let autoLogin = false
console.log('登录portainer...');
if (cache.username && cache.password) {
const r = await q('自动登录(y/n)')
if (!r || r === 'y') {
username = cache.username
password = cache.password
autoLogin = true
}
}
if (!autoLogin) {
username = await q('请输入用户名:')
password = await q('请输入密码:')
}
rl.close()
// 登录portainer
const { data } = await axios.post('/auth', {
username,
password
})
cache.token = data.jwt
const obj = [
`username=${username}`,
`password=${password}`,
`token=${data.jwt}`
]
// 写入缓存
fs.writeFileSync(`../${config.env == 'local' ? '.deploy/' : ''}.cache`, obj.join('
'))
}
console.log('打包构建...');
execSync(config.buildScript, {stdio: 'inherit'}) // 打包测试环境,执行命令以实际项目为准
console.log('获取容器列表...');
const { data } = await axios.get('/endpoints/3/docker/containers/json?all=1')
const currentContainer = data.find(i => i.Names.includes(`/${config.container}`))
if (!currentContainer) {
console.log('未找到对应容器,请检查容器名称');
return
}
console.log('获取容器配置...');
const { data: currConfig } = await axios.get(`/endpoints/3/docker/containers/${currentContainer.Id}/json`)
let imageName = currConfig.Config.Image.split(':')[0] + ":" + packageJson.version
if (config.env == 'prod') {
imageName = `harbor.saxofintech.com/online/westmoney/${packageJson.name}:${packageJson.version}` // 生产镜像
}
console.log(`镜像名称:${imageName}`);
// 未自动上传镜像,上传一次
if (!fs.existsSync(`./dist/dist.tar`)) {
process.chdir('./dist')
execSync('tar -cvf dist.tar *')
console.log('生成压缩文件dist.tar...');
await new Promise(r => setTimeout(() => r(), 3000))
console.log('上传镜像...');
const distTar = fs.readFileSync('./dist.tar')
await axios.post(`/endpoints/3/docker/build?dockerfile=Dockerfile&t=${imageName}`, distTar, {
headers: {
'Content-Type': 'application/x-tar'
}
})
}
if (config.env == 'prod') {
console.log('推送镜像...');
await axios.post(`/endpoints/2/docker/images/${imageName.replace(///g, encodeURIComponent('/'))}/push`, {
imageName,
}, {
headers: {
'X-PortainerAgent-Target': 'wm-vm-h5-sit',
'X-Registry-Auth': Buffer.from('{"registryId":1}').toString('base64')
}
})
execSync(`echo ${imageName} | clip`),// windows下可用
console.log('推送完成,已复制镜像名到剪贴板');
return
}
console.log('暂停当前容器...');
await axios.post(`/endpoints/3/docker/containers/${currentContainer.Id}/stop`)
console.log('重命名当前容器为old...');
await axios.post(`/endpoints/3/docker/containers/${currentContainer.Id}/rename?name=${config.container}-old`)
const params = {
...currConfig.Config,
Image: imageName,
HostConfig: currConfig.HostConfig,
name: config.container,
}
// console.log(params);
console.log('新建容器...');
const { data: { Id, Portainer } } = await axios.post(`/endpoints/3/docker/containers/create?name=${config.container}`, params)
console.log('配置容器权限...');
await axios.put(`/resource_controls/${Portainer.ResourceControl.Id}`, {
AdministratorsOnly: false,
Public: false,
Teams: [1], // 组先默认为1
Users: []
})
console.log('启动容器...');
await axios.post(`/endpoints/3/docker/containers/${Id}/start`,{})
console.log('删除old容器...');
await axios.delete(`/endpoints/3/docker/containers/${currentContainer.Id}?v=1&force=true`)
console.log('发布完成!');
} catch (err) {
console.log(err);
}
}
deploy()
2、修改容器名称及打包测试环境的命令
...
const config = {
container: 'simulated-trade-h5-test-container', // 容器名称
buildScript: process.env.DEPLOY_ENV == 'prod' ? 'npm run build:pro' : 'npm run build:test', // 打包命令
apiUrl: 'http://10.156.160.11:9000/api', // portainer接口地址
env: process.env.DEPLOY_ENV || 'test', // ['local', 'test', 'prod'] 设置为local可通过本地代码进行部署,
}
3、执行该js文件即可发布测试环境
node deploy.js
4、也可将执行命令加入npm script中,通过环境变量切换打包模式
// package.json
"scripts": {
...
"deploy": "node deploy.js",
"deploy:local": "cross-env DEPLOY_ENV=local node deploy.js",
"deploy:prod": "cross-env DEPLOY_ENV=prod node deploy.js",
...
},
DEPLOY_ENV=local: 打包本地代码发布测试环境
DEPLOY_ENV=test: 拉取远端代码打包测试环境
DEPLOY_ENV=prod: 打包生产并上传镜像(将镜像名发给运维发布)
流程图
实现原理
-
首先需要创建一个单独的环境,防止被本地开发的代码影响。在当前项目的上级目录新建一个.deploy目录,用于发布指定项目。
-
进到deploy目录拉取项目代码,并切换到当前分支
// 远程仓库地址
const url = execSync('git config --get remote.origin.url')
const dirname = __dirname.split(/[\/]/)
// 当前文件夹名称
const folder = dirname.slice(-1).toString()
// 当前分支
const branch = execSync("git branch --show-current")
// 创建副本仓库
process.chdir('../')
if (!fs.existsSync(`.deploy`)) {
fs.mkdirSync('.deploy')
}
process.chdir('./.deploy')
if (!fs.existsSync(folder)) {
execSync(`git clone ${url} ${folder}`, {stdio: 'inherit'})
}
...
if (config.env == 'local') {
console.log('本地仓库环境');
process.chdir(`../${folder}`)
} else {
console.log('远程仓库环境');
process.chdir(folder)
execSync(`git fetch --all && git reset --hard origin/${branch}`, {stdio: 'inherit'})
console.log('安装依赖...');
execSync('npm i', {stdio: 'inherit'})
}
-
登陆portainer, 同时对登录信息进行缓存,将用户名密码及token缓存到.cache文件中
-
const checkToken = async () => { const token = cache.token if (!token) { return false } try { await axios.get('/endpoints/3/docker/containers/json?all=1') } catch { return false } return true } ... const hasToken = await checkToken() if (!hasToken) { // 读取用户输入 const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const q = (str) => new Promise(resolve => { rl.question(str, res => { resolve(res) }) }) let username = '' let password = '' let autoLogin = false console.log('登录portainer...'); if (cache.username && cache.password) { const r = await q('自动登录(y/n)') if (!r || r === 'y') { username = cache.username password = cache.password autoLogin = true } } if (!autoLogin) { username = await q('请输入用户名:') password = await q('请输入密码:') } rl.close() // 登录portainer const { data } = await axios.post('/auth', { username, password }) cache.token = data.jwt const obj = [ `username=${username}`, `password=${password}`, `token=${data.jwt}` ] // 写入缓存 fs.writeFileSync(`../${config.env == 'local' ? '.deploy/' : ''}.cache`, obj.join(' ')) }
-
执行打包
console.log('打包构建...');
execSync(config.buildScript, {stdio: 'inherit'}) // 打包测试环境,执行命令以实际项目为准
-
调用portainer容器相关接口,完成发布
console.log('获取容器列表...');
const { data } = await axios.get('/endpoints/3/docker/containers/json?all=1')
const currentContainer = data.find(i => i.Names.includes(`/${config.container}`))
if (!currentContainer) {
console.log('未找到对应容器,请检查容器名称');
return
}
console.log('获取容器配置...');
const { data: currConfig } = await axios.get(`/endpoints/3/docker/containers/${currentContainer.Id}/json`)
let imageName = currConfig.Config.Image.split(':')[0] + ":" + packageJson.version
if (config.env == 'prod') {
imageName = `harbor.saxofintech.com/online/westmoney/${packageJson.name}:${packageJson.version}` // 生产镜像
}
console.log(`镜像名称:${imageName}`);
// 未自动上传镜像,上传一次
if (!fs.existsSync(`./dist/dist.tar`)) {
process.chdir('./dist')
execSync('tar -cvf dist.tar *')
console.log('生成压缩文件dist.tar...');
await new Promise(r => setTimeout(() => r(), 3000))
console.log('上传镜像...');
const distTar = fs.readFileSync('./dist.tar')
await axios.post(`/endpoints/3/docker/build?dockerfile=Dockerfile&t=${imageName}`, distTar, {
headers: {
'Content-Type': 'application/x-tar'
}
})
}
if (config.env == 'prod') {
console.log('推送镜像...');
await axios.post(`/endpoints/2/docker/images/${imageName.replace(///g, encodeURIComponent('/'))}/push`, {
imageName,
}, {
headers: {
'X-PortainerAgent-Target': 'wm-vm-h5-sit',
'X-Registry-Auth': Buffer.from('{"registryId":1}').toString('base64')
}
})
execSync(`echo ${imageName} | clip`),// windows下可用
console.log('推送完成,已复制镜像名到剪贴板');
return
}
console.log('暂停当前容器...');
await axios.post(`/endpoints/3/docker/containers/${currentContainer.Id}/stop`)
console.log('重命名当前容器为old...');
await axios.post(`/endpoints/3/docker/containers/${currentContainer.Id}/rename?name=${config.container}-old`)
const params = {
...currConfig.Config,
Image: imageName,
HostConfig: currConfig.HostConfig,
name: config.container,
}
// console.log(params);
console.log('新建容器...');
const { data: { Id, Portainer } } = await axios.post(`/endpoints/3/docker/containers/create?name=${config.container}`, params)
console.log('配置容器权限...');
await axios.put(`/resource_controls/${Portainer.ResourceControl.Id}`, {
AdministratorsOnly: false,
Public: false,
Teams: [1], // 组先默认为1
Users: []
})
console.log('启动容器...');
await axios.post(`/endpoints/3/docker/containers/${Id}/start`,{})
console.log('删除old容器...');
await axios.delete(`/endpoints/3/docker/containers/${currentContainer.Id}?v=1&force=true`)
console.log('发布完成!');
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。