0. Intro

先介绍问题,现在开发往往都是基于git的vcs,然后很多小公司并没有完整的CI/CD工具和独立的测试团队,再加上一些历史问题,所以往往会产生一种开发完只能在测试服务器上测试页面效果的流程。

为了应对这种流程不同的公司也有不同的套路,首先是基于git branch的开发,但是因为测试服务器往往数量有限,所以可能会出现要等待另一个人测试完成的情况。还有一些公司更简单,想测试的时候就把未开发完成想看看效果的代码先合并到develop分支进行部署,这种情况往往会搞到git history很糟糕还有就是容易搞崩测试服务器。

1. 解决

其实在docker如此方便的时候处理这种情况已经很简单了,我们可以自己开发个简单的系统来处理一下:

  • 提交代码到git同时在commit message里包含@staging
  • 代码库触发webhook到我们自己的系统
  • 我们系统检出相关代码并启动一个container来运行,同时自动绑定一个随机二级域名
  • 动态代理随机域名

还可以做一些其他的自定义的功能:

  • 手动绑定测试二级域名到指定分支。
  • 统计测试情况
  • 收集测试log检测异常
  • 可以用@remove触发删除

2. Gitlab Webhook

gitlab的webhook可以有很多种触发,我们只需要选择push事件。


func webhook(w http.ResponseWriter, r *http.Request){
  ...
  for _,v:=gitlabRequest["commits"] { // 每个push webhook包含多个commit
    if strings.Contains(v["message"],"@staging") {
      if !utils.CheckDeployed(v){ // 检测有没有部署,可以通过commit id来判断
        dc := storage.NewDeployContainer(v) // 先构建一个struct用来存储信息 storage可自行选择,包括redis,etcd,甚至用于测试的只写个结构体
        workers.Push(workers.Job{
          Key:    "container.deploy",
          Params:  dc,
        }
      }
    }
  }
  ...
}

3. Worker

func (job Job)Run(){
  ...
  container := job.Params.(storage.Container)
  prepareDir(container.DeployPath) // 创建部署dir
  fetchCode(&container) // clone 代码
  cmd := exec.Command("sh", filepath.Join(container.DeployPath, "current", "build.sh"), filepath.Join(container.DeployPath, "current"), container.Name+":"+container.ID, container.ID) // 执行代码下build.sh
  cmd.Stdout = os.Stdout
  cmd.Stderr = os.Stderr
  cmd.Run()
  address := utils.DockerAddress(container.ID) // 获取地址
  container.Address = address
  container.Save() // 运行之后保存container到storage
  ...
}

// fetch code from gitlab

可以用gopkg.in/src-d/go-git.v4来处理对git的请求。


func fetchCode(container *storage.Container) {
	pem, _ := ioutil.ReadFile(config.Config.SSHPrivateKey)
	signer, _ := ssh2.ParsePrivateKey(pem)
	repo, err := git.PlainClone(filepath.Join(container.DeployPath, "current"), false, &git.CloneOptions{
		ReferenceName: plumbing.ReferenceName(container.Ref),
		Progress:      os.Stdout,
		URL:           container.GitUrl,
		Auth:          &ssh.PublicKeys{User: "git", Signer: signer},
	})
	utils.CheckError(err)
	head, _ := repo.Head()
	commit_hash := head.Hash()
	container.Commit = commit_hash.String()
	commit, _ := repo.CommitObject(commit_hash)
	container.Author = commit.Author.Name
}

对于docker来说,因为docker本身就可以算是基于http的,所以写个client直接请求dockerd的api即可:

func DockerDelete(cid string) {
	var resp *http.Response
	req, _ := http.NewRequest("DELETE", "http://unix/containers/"+cid+"?force=true", nil)
	resp, _ = client().Do(req)
	defer resp.Body.Close()
}

func client() *http.Client {
	return &http.Client{
		Transport: &http.Transport{
			DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
				return net.Dial("unix", config.Config.DockerSock)
			},
		},
	}
}

4. Proxy

对于请求来说,每次我们启动或者新增一个自定义域名新增一个代理服务器:

func NewReverseProxy(target string) *httputil.ReverseProxy {
	return httputil.NewSingleHostReverseProxy(&url.URL{
		Scheme: "http",
		Host:   target,
	})
}
proxies[container.ID] = NewReverseProxy(container.Address)
// proxies[container.Name] = proxies[container.ID]

然后在请求的时候获取一下二级域名即可:

type PorxyServer struct{}
func (ProxyServer)ServeHTTP(w http.ResponseWriter, r *http.Request){
  subDomain := utils.GetSubdomain(r)
  proxies[subDomain].ServeHTTP(w, r)
}
ps := ProxyServer{}
http.ListenAndServe(":8080", ps)

5. 其他

这个有很多成熟的解决方案,例如jenkins,而且上面描述的也只是一个玩具系统,不过是个好玩的玩具系统,自己造轮子的好处就在于完全控制和学习新知识。