您现在的位置是:首页 >技术杂谈 >测试环境一键发布网站首页技术杂谈

测试环境一键发布

Jason_wu86 2024-07-22 00:01:02
简介测试环境一键发布

背景

目前公司项目发布测试环境不够自动化,每次需要手动打包并且手动更新,影响开发效率

流程图

 

而且因为是本地手动发布,容易失误造成一些不必要的麻烦:

  • 远端代码有更新,忘记拉取代码

  • 快速发布,本地代码忘了提交,导致远端代码缺失

  • 误将本地的一些测试代码发布

...

因此,如果能够将操作脚本化,不仅可以提高开发效率,同时也能减少失误造成的影响。

一键发布

使用

  1. 先将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('发布完成!');

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。