• 为容器化的 Go 程序搭建 CI


    本文介绍如何使用 Jenkins 的声明式 pipeline 为一个简单的 Golang web 应用搭建 CI 环境。如果你还不太了解 Jenkins 及其声明式 pipeline,请先参考笔者的 Jenkins 系列文章,或者直接到 Jenkins 官网进行学习。说明:本文的演示环境为 ubuntu 16.04。

    准备 Jenkins 环境

    鉴于篇幅原因,本文不再介绍 Jenkins 环境的搭建。本文演示的 demo 只要求 Jenkins server 连接了一个带有 go 标签的 agent,该 agent 上安装了 docker:

    如果你希望可以收到 CI 中的邮件通知,请配置 Jenkins 邮件通知中的 SMTP server。

    demo 程序

    笔者创建了一个简单的 Golang web 程序用于演示,大家可以从这里下载该程序。
    app.go
    app.go 文件包含主程序,其内容如下:

    package main
    
    import (
        "fmt"
        "net/http"
        "strings"
    )
    
    func getNameLen(name string) int {
        return len(name)
    }
    
    func sayHello(w http.ResponseWriter, r *http.Request) {
        message := r.URL.Path
        message = strings.TrimPrefix(message, "/")
        message = "Hello " + message + " : " + fmt.Sprintf("%d", getNameLen(message))
        w.Write([]byte(message))
    }
    
    func main() {
        http.HandleFunc("/", sayHello)
        if err := http.ListenAndServe(":8088", nil); err != nil {
            panic(err)
        }
    }

    该程序的功能非常简单,如果你在 url 中域名后面的部分添加了自己的名字,它会向你问好并计算出你名字的长度:

    app_test.go
    app_test.go 文件包含了函数 getNameLen() 的单元测试:

    package main
    
    import (
        "testing"
    )
    
    func Test_GetNameLen_1(t *testing.T) {
        if l := getNameLen("nick"); l != 4 {
            t.Error("test failed, the length of nick is not correct.")
        } else {
            t.Log("test passed.")
        }
    }
    
    func Test_GetNameLen_2(t *testing.T) {
        if l := getNameLen(""); l != 0 {
            t.Error("test failed, the length of empty string is not correct.")
        } else {
            t.Log("test passed.")
        }
    }

    Dockerfile
    Dockerfile 文件用于构建 docker 镜像,其内容如下:

    FROM golang:1.11.0
    WORKDIR /go/src/gowebdemo/
    RUN go get -d -v golang.org/x/net/html
    COPY app.go .
    RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o gowebdemo .
    
    FROM alpine:latest
    RUN apk --no-cache add ca-certificates
    WORKDIR /root/
    COPY --from=0 /go/src/gowebdemo .
    EXPOSE 8088
    CMD ["./gowebdemo"]

    在准备好上面的内容后,让我们开始 CI 的配置。

    Jenkinsfile

    为了实现 pipeline as code,我们把配置 Jenkins 的 pipeline 内容保存到 Jenkinsfile 文件中,并和代码一起 checkin 到代码中。该 demo 的 Jenkinsfile 内容如下:

    pipeline {
        agent {
            label 'go'
        }
        stages {
            stage('UnitTest') {
                steps {
                    script {
                        if( sh(script: 'docker run --rm -v $(pwd):/go/src/gowebdemo -w /go/src/gowebdemo golang:1.11.0 /bin/bash -c "/go/src/gowebdemo/rununittest.sh"', returnStatus: true ) != 0 ){
                           currentBuild.result = 'FAILURE'
                        }
                    }
                    junit '*.xml'
                    script {
                        if( currentBuild.result == 'FAILURE' ) {
                           sh(script: "echo unit test failed, please fix the errors.")
                           sh "exit 1"
                        }
                    }
                }
            }
            stage('Build') {
                steps {
                    sh './buildapp.sh'
                }
            }
            stage('Deploy') {
                steps {
                    sh './deployapp.sh'
                }
            }
        }
        post {
            failure {
                mail bcc: '', body: "<b>gopro build failed</b><br>Project: ${env.JOB_NAME} <br>Build Number: ${env.BUILD_NUMBER} <br> URL de build: ${env.BUILD_URL}", cc: '', charset    : 'UTF-8', from: '', mimeType: 'text/html', replyTo: '', subject: "ERROR CI: Project name -> ${env.JOB_NAME}", to: "your email address";
            }
            success {
                mail bcc: '', body: "<b>gopro build success</b><br>Project: ${env.JOB_NAME} <br>Build Number: ${env.BUILD_NUMBER} <br> URL de build: ${env.BUILD_URL}", cc: '', charset: 'UTF-8', from: '', mimeType: 'text/html', replyTo: '', subject: "SUCCESS CI: Project name -> ${env.JOB_NAME}", to: "your email address";
            }
        }
    }

    label 'go'
    agent 中的 label 指定该 pipeline 运行在带有 go 标签的 agent 上。

    stage('UnitTest')
    该部分运行代码中的单元测试,并根据单元测试的结果确定是否继续执行后面的流水线操作。其中的脚本文件 rununittest.sh 内容如下:

    #!/bin/bash
    
    set -x
    go get -d -v golang.org/x/net/html
    go get -u github.com/jstemmer/go-junit-report
    go test -v 2>&1 > tmp
    status=$?
    $GOPATH/bin/go-junit-report < tmp > test_output.xml
    
    exit ${status}

    该脚本执行单元测试操作,并把运行单元测试命令的结果作为脚本运行的结果返回。这一点很重要,我们就是通过这种方式来知道单元测试是否完全通过,如果没有完全通过就让该次持续集成失败,而停止后续的操作。同时使用 go-junit-report 组件把单元测试的结果保存为 junit 格式的文件 test_output.xml。junit '*.xml' 则可以分析该单元测试的结果,并以图表的方式展示:

    stage('Build')
    该部分执行脚本 buildapp.sh,其内容如下:

    #!/bin/bash
    
    set -ex
    docker build -t gowebdemo .
    
    # remove all none tag images
    if [ ! -z "$(docker images -q --filter 'dangling=true')" ]; then
      docker rmi $(docker images -q --filter "dangling=true")
    fi

    首先执行 docker build -t gowebdemo . 命令,以 Dockerfile 中的指令构建应用程序并打包为容器镜像。然后移除系统中没有标签的镜像释放磁盘空间。
    在比较正式的环境中,一般会把构建好的容器镜像推送到私有的镜像库中,这里为了简化过程,就把镜像存放在 agent 上,并在下一步中在 agent 上部署一个应用的实例。

    stage('DeployApp')
    该部分执行脚本 deployapp.sh,脚本的内容如下:

    #!/bin/bash
    
    set -ex
    # remove the current app instance
    if [ -n "$(docker ps -aq -f name=nickwebdemo)" ]; then
        docker rm -f nickwebdemo
    fi
    
    # run a new app instance
    docker run -d 
        -p 8088:8088 
        --name nickwebdemo 
        --restart=always 
        gowebdemo

    脚本先检查是不是已经有同名的容器实例,如果有就先删除掉该实例,然后运行一个新的实例。

    在最后的 post 部分,我们根据该次持续集成的状态来发送不同的邮件通知,比如整个过程没有错误发生,单元测试也都通过了,就发送成功的通知,否则发送失败的通知。

    配置 Jenkins Job

    在 Jenkins 中创建 pipeline 类型的 Job,并设置从 SCM 获得 pipeline 脚本:

    因为笔者放置代码的库是公开的,所以只用指定代码库的路径就可以了,不需要添加相关的认证信息。
    现在就可以触发 CI 过程了,下图是笔者机器上运行完成后的截图:

    虽然只有两个单元测试的 case,但显示的结果还不错!并且 web app 被成功的部署到了 agent 上。

    checkin 代码进行演示

    下面我们配置 Jenkins 每隔一分钟检查一次代码是否有变更,有的话就触发 CI。在 Build Triggers 中选择 Poll SCM,然后输入 5 个由空格分隔的 * 号:

    保存该的配置,接下来让我们添加一个单元测试的 case:

    func Test_GetNameLen_3(t *testing.T) {
        if l := getNameLen("andrew"); l != 5 {
            t.Error("test failed, the length of andrew string is not correct.")
        } else {
            t.Log("test passed.")
        }
    }

    这里笔者故意算错了字符串 "andrew" 的长度,checkin 这段代码,然后看看 Jenkins 中持续集成的过程:

    持续集成的过程被自动触发了,但是由于单元测试中有失败的 case 导致整个过程都失败了,并且单元测试后面的过程都没有被执行:

    如果你正确配置了邮件服务器并且把 Jenkinsfile 中的邮件地址改成的你自己的邮件地址,那么不管持续集成是成功还是失败你都会收到相关的通知。
    现在把失败的单元测试修改正确,再提交一次,这样就开启了我们的持续集成之旅!

    总结

    本文只是介绍了一个非常简单的 demo 场景,但是一旦一个简单的环境能够运行起来了,你就可以不断的往上添砖加瓦,比如创建集成测试的环境,添加集成测试,并最终销毁集成测试环境等内容,最终让它成为一个能够满足需求的持续集成流水线。

    参考:
    Building a CI for Golang test
    Building a CI system for Go, with Jenkins

  • 相关阅读:
    block、inline、inline-block
    js 的复制和引用 (传值和传址)
    俄罗斯方块和作品集
    js 连续赋值。。理解不了,先占坑
    8.7 jquery-dom manipulation
    08.04 对象构造方法
    对象的基本操作
    08.03 js _oop
    08.02 对象
    The 'Microsoft.ACE.OLEDB.12.0' provider is not registered on the local machine.
  • 原文地址:https://www.cnblogs.com/sparkdev/p/9865473.html
Copyright © 2020-2023  润新知