DevOps编程操练:用Jenkins流水线建立代码质量预警机制

2023-09-23 16:18:36 浏览数 (1)

解决痛点

  • 不知如何用docker搭建Jenkins操练环境
  • 不知如何开始为Java代码编写自动化单元测试
  • 不知如何将单元测试运行在Jenkins流水线上
  • 不知如何将繁琐的手工Jenkins流水线配置,简化为编写一个Jenkinsfile脚本,并进行版本控制
  • 当流水线出现故障后,不知如何revert导致故障的代码提交,来解决故障

使用docker搭建Jenkins操练环境

当然也可以不用docker,直接在本机安装Jenkins。但对于操练DevOps技能来说,Docker是一个必修项目。所以本操练使用docker来搭建操练环境

本操练是从“CI搭建兽”到“流水线即代码”的升级版,除了使用docker来运行Jenkins之外,还将 Jenkinsfile的写法,从原来的脚本式(以 node 开头),升级为声明式(以 pipeline开头)

安装docker

参见 Install Docker Engine安装Docker

下面以Ubuntu 20.04为例进行操练,其他操作系统操练步骤类同

安装Kitematic

Kitematic是一个为了方便使用docker而精心设计的图形化工具。参见Kitematic发布页面安装Kitematic

安装Jenkins

在Kitematic里下载jenkins/jenkins的image,启动容器并安装Jenkins

打开Kitematic,在搜索框中输入 jenkins来搜索所有Jenkins的镜像。选择镜像名字第一行和第二行都是jenkins的那个镜像。点击CREATE 按钮下载镜像,并启动容器。参见下图

点击 CREATE 按钮下载镜像,并启动容器

点击左上角 jenkins 容器,然后点击右上角 Settings 页签,将容器改名为jenkins-kata ,参见下图

将容器改名为 jenkins-kata

点击右上角 Home 页签,浏览容器的log,等待jenkins重启

在本机创建文件夹~/OOR/docker-volumes/jenkins-kata,并将其配置为docker的volume,以便保存Jenkins运行后的输出文件,且能同时被docker和本机访问。参见下图

设置docker的volume

点击右上角 Home 页签,浏览容器的log,等待jenkins重启

点击右上角 Settings 页签,再点击下面左侧的 Hostname/Ports页签,记下页面左侧中间第一个带有 localhost 的端口号,如下图所示的localhost:32769,然后打开浏览器,在地址栏中访问这个地址和端口号,就能进入Jenkins安装页面,安装Jenkins。安装第一步所需要的admin管理员密码,能在Home页签中的log内容中找到。安装Jenkins插件时,选择默认的即可。参见下图

查看Jenkins运行的端口号

用spring boot编写一个web应用程序并手工测试

本操练的代码和文档参见

devops-katas-jenkins-pipeline-as-code-kata

从 start.spring.io 下载web空白应用

下载前的选项,参见下面的列表。其中Dependencies添加Web

  • Group: devops.katas
  • Artifact: adminprovider
  • Name: adminprovider
  • Description: Demo project for Jenkins pipeline as code
  • Dependencies: Web

编写adminprovider的Web应用,可以按id号一次返回一位管理员

将刚才下载的adminprovider.zip解压,用IntelliJ IDEA打开该Maven项目,开始编写一个Web应用

为方便起见,本操练所创建的类,都写在AdminproviderAppication类中

首先创建 AdminController

AdminproviderApplication.java.

代码语言:javascript复制
@RestController
class AdminController {
        @GetMapping("/admin/{id}")
        Admin admin(@PathVariable int id) {
                return new Admin("firstName ["   id   "]", "lastName ["   id   "]");
        }
}

然后创建 Admin类。其中的两个getter是必须的,否则在运行时会报HttpMessageNotWritableException

AdminproviderApplication.java.

代码语言:javascript复制
class Admin {

        private final String firstName;
        private final String lastName;

        public Admin(String firstName, String lastName) {
                this.firstName = firstName;
                this.lastName = lastName;
        }

        // The public getters are mandatory to fix the issue "org.springframework.http.converter.
        // HttpMessageNotWritableException: No converter found for return value of type:
        // class devops.katas.adminprovider.Admin"
        public String getFirstName() {
                return firstName;
        }

        public String getLastName() {
                return lastName;
        }
}

最后在 application.properties 文件中,添加该Web应用启动的端口号 8765

application.properties.

代码语言:javascript复制
server.port=8765

此时,在Intellij IDEA中运行 AdminproviderApplication类。然后用浏览器或 HTTPie工具来访问地址localhost:8765/admin/1 。应该能得到1号管理员的姓和名,参见下图

用HTTPie工具访问

编写AdminService的自动化单元测试

为了让Jenkins流水线起到质量预警的作用,必须在上面运行自动化测试,来检测每一次代码push是否有缺陷。让我们先从单元测试开始。

目前要测试的单元,是根据 id 号生成 Admin 对象。这段逻辑写在了AdminController

类中,而这个设计是不好的。因为Controller类本来的用途,是起“传达室”的作用,即将用户的请求,分配给相应的服务来处理。所以良好的设计,应该是把这段逻辑交给AdminService 来处理。而对这段逻辑的单元测试,也就是对 AdminService的单元测试。

第一步,先把上述逻辑交给 AdminService 来处理

AdminproviderApplication.java.

代码语言:javascript复制
@Configuration
class AdminConfiguration {
        @Bean
        AdminService adminService() {
                return new AdminService();
        }
}

class AdminService {
        public Admin retrieveAdmin(int id) {
                return new Admin("firstName ["   id   "]", "lastName ["   id   "]");
        }
}

@RestController
class AdminController {
        @Autowired
        AdminService adminService;

        @GetMapping("/admin/{id}")
        Admin admin(@PathVariable int id) {
                return adminService.retrieveAdmin(id);
        }

}

第二步,为 AdminService 编写单元测试

AdminServiceTest.java.

代码语言:javascript复制
class AdminServiceTest {
    @Test
    public void should_retrieve_an_admin_with_correct_names() {
        AdminService adminService = new AdminService();

        Admin admin = adminService.retrieveAdmin(4);

        BDDAssertions.then(admin.getFirstName()).isEqualTo("firstName [4]");
        BDDAssertions.then(admin.getLastName()).isEqualTo("lastName [4]");
    }

}

在IntelliJ IDEA中运行单元测试,应该运行通过

现在可以把上述代码push到码云中,以便后面操练中的Jenkins流水线读取代码来运行自动化测试

可以在码云自己的帐号中,创建一个名为devops-katas-jenkins-pipeline-as-code-kata

的空的代码库。然后在代码根目录中,使用下述命令push代码

代码语言:javascript复制
git init
git add .
git commit -m "AdminService with a test"
git remote add origin https://gitee.com/wubin28/devops-katas-jenkins-pipeline-as-code-kata.git
git push -u origin master

本文代码的码云地址为 https://gitee.com/wubin28/devops-katas-jenkins-pipeline-as-code-kata.git

下面的任务,就是要把上述单元测试,运行在Jenkins流水线上

在Jenkins界面上编写流水线脚本并运行流水线

虽然本操练的最终目标,是要用Jenkinsfile脚本来定义流水线,但为了调试脚本方便,所以先在Jenkins界面上把脚本调试好,然后再把这些脚本写入Jenkinsfile

创建文件夹

为方便管理操练内容,首先在Jenkins主页上创建jenkins-pipeline-as-code-kata文件夹,以后的操作都在该文件夹中

点击 New Item

点击 New Item

创建文件夹

创建文件夹

不需要配置,直接点 Save

不需要配置,直接点 Save

文件夹创建完毕

文件夹创建完毕

确认Maven与git都已经在Jenkins中配置好

因为运行流水线需要Maven和Git这两个工具,所以需要事先在Jenkins里配置好

进入 Global Tool Configuration 页面

进入 Global Tool Configuration 页面

把Maven命名为M3

把Maven命名为M3

把git命令在Jenkins容器里的路径设置为 /usr/bin/git。这一点可以通过执行命令 docker container exec -it jenkins-katas bash进入容器内部查看,查看有按 Ctrl PQ 退出

把git命令的路径设置为 /usr/bin/git

创建名为adminprovider的流水线

进入jenkins-pipeline-as-code-kata文件夹,点击 New Item ,创建名为adminprovider 的流水线

创建名为 adminprovider 的流水线

修改流水线的脚本

在流水线配置页面的底部, script 输入框的右上角try sample Pipeline... ,选择 GitHub Maven

流水线样例脚本,作为修改的基础

选择 GitHub Maven流水线样例脚本,作为修改的基础

将第13行的git代码库的地址改为本操练的代码库的地址 https://gitee.com/wubin28/devops-katas-jenkins-pipeline-as-code-kata.git

将第13行的git代码库的地址改为本操练的代码库的地址

将第16行的mvn命令,改为./mvnw clean package'。mvnw命令能够在没有安装maven的情况下,运行maven命令。之后,点击 `Save按钮保存

将第16行的mvn命令,改为 ‘./mvnw clean package’

点击 Build Now 手工触发流水线构建。点击左下角 #1 左侧的小圆点,能够跳转到控制台输出页面,观察运行结果。

点击 Build Now 手工触发流水线构建

点击左下角 #1左侧的小圆点,能够跳转到控制台输出页面

如果一切正常,那么构建应该成功。这表明在界面上编写的脚本没有问题。下面可以把这些脚本写到

Jenkinsfile文件中,以便让Jenkins读取该文件中的流水线配置信息。从而实现用Jenkinsfile脚本文件来定义流水线,减轻配置的工作量。

根据脚本创建Jenkinsfile,并配置Jenkins,使其读取Jenkinsfile来运行流水线

因为流水线脚本要从git版本库中读取,需要重新配置,所以现在创建一个名为adminprovider-from-scm新的流水线

创建名为adminprovider-from-scm的流水线

准备好Jenkinsfile

在流水线配置页面的底部, script 输入框的右上角 try sample Pipeline... ,选择 GitHub Maven

流水线样例脚本,将其内容复制粘贴到代码根目录下新创建的Jenkinsfile文件中,并把其中的git版本库地址和maven命令如上所示更改过来。为了验证Jenkins确实从Jenkinsfile读取了流水线配置,在 steps 第一句增加了 echo 'hello from scm。修改完Jenkinsfile后,就可以点击流水线配置页面底部的 Save按钮,保存配置。

Jenkinsfile.

代码语言:javascript复制
pipeline {
    agent any

    tools {
        // Install the Maven version configured as "M3" and add it to the path.
        maven "M3"
    }

    stages {
        stage('Build') {
            steps {
                echo 'hello from scm'
                // Get some code from a GitHub repository
                git 'https://gitee.com/wubin28/devops-katas-jenkins-pipeline-as-code-kata.git'

                // Run Maven on a Unix agent.
                sh "./mvnw clean package"

                // To run Maven on a Windows agent, use
                // bat "mvn -Dmaven.test.failure.ignore=true clean package"
            }

            post {
                // If Maven was able to run the tests, even if some of the test
                // failed, record the test results and archive the jar file.
                success {
                    junit '**/target/surefire-reports/TEST-*.xml'
                    archiveArtifacts 'target/*.jar'
                }
            }
        }
    }
}

使用以下命令,将代码push到git版本库

代码语言:javascript复制
git add .
git commit -m "add Jenkinsfile"
git pull --rebase
git push -u origin master

配置Jenkins使其读取代码库中的Jenkinsfile来配置流水线

进入刚刚创建的流水线 adminprovider-from-scm 配置页面,在页面底部的

Pipeline 配置区域,点击 Definition 下拉框,选择

Pipeline script from SCM

选择Pipeline script from SCM

SCM 下拉框中,选择 Git。在 Repository URL中,填入Jenkinsfile所在的代码库的地址

https://gitee.com/wubin28/devops-katas-jenkins-pipeline-as-code-kata.git。确保Branch Specifier 中填写了 */masterScript Path 中填写了Jenkinsfile 。点击 Save 保存

选择 Git,填写代码库地址

点击 Build Now

手工触发流水线构建,让Jenkins读取代码库中的Jenkinsfile。

点击 Build Now手工触发流水线构建

点击左下角 #1

左侧的小圆点,能够跳转到控制台输出页面,观察运行结果中包含了上面添加的那句 hello from scm 。说明Jenkins确实读取了Jenkinsfile

观察运行结果中包含了上面添加的那句 hello from scm

触发流水线

现在Jenkins能从代码库中读取Jenkinsfile了。这意味着流水线的配置,都可以用有版本控制的脚本来完成。但如果想让Jenkins定时轮询代码库,以便做到频繁小批地构建代码,从而尽早频繁小批地定位代码质量问题,更容易地修复问题,这该如何用脚本实现呢?(当然,使用web hook会比轮询更有优势——能实现代码库一旦有代码push上来,就能通知Jenkins进行构建,从而把频繁小批构建做到极致。有关web hook的操练,我们以后再做)

在jenkinsfile中配置轮询

为了验证Jenkins对代码库的轮询,确实来自Jenkinsfile,可以先打开流水线配置页面中的build

trigger配置,确认没有任何选项被勾选了

打开流水线配置页面中的build trigger配置,确认没有任何选项被勾选了

在Jenkinsfile中的 agent any 下面,添加五个星号的 cron,表示Jenkins每隔1分钟就轮询一次代码库,无论是否有新代码,都会执行构建

代码语言:javascript复制
triggers {
  cron('* * * * *')
}

使用以下命令,将代码push到git版本库

代码语言:javascript复制
git commit -am "add triggers with 5 stars into Jenkinsfile"
git pull --rebase
git push -u origin master

点击 Build Now手工触发流水线构建,让Jenkins读取代码库中的Jenkinsfile

确认流水线配置页面中的Build Triggers配置区域中,Build periodically已经被勾选,且五个星出现在 Schedule输入框中。这表明Jenkins确实读取了Jenkinsfile

确认流水线配置页面中的Build Triggers配置区域中,Build periodically已经被勾选,且五个星出现在 Schedule 输入框中

在流水线上引入一个编译错误,并revert来解决问题

现在操练一下当流水线遇到编译错误时,会报什么错

在测试代码中,加一句 abc(); ,然后push代码到代码库

AdminServiceTest.java.

代码语言:javascript复制
class AdminServiceTest {
    @Test
    public void should_retrieve_an_admin_with_correct_names() {
        abc();
        AdminService adminService = new AdminService();

        Admin admin = adminService.retrieveAdmin(4);

        BDDAssertions.then(admin.getFirstName()).isEqualTo("firstName [4]");
        BDDAssertions.then(admin.getLastName()).isEqualTo("lastName [4]");
    }
}

等1分钟后,流水线被轮询程序自动触发。把鼠标放到有提交的出错构建处,能看到导致这次构建失败的提交人和提交信息。点击相应提交左边的小圆球,能看到具体的错误信息

等1分钟后,流水线被轮询程序自动触发。把鼠标放到有提交的出错构建处,能看到导致这次构建失败的提交人和提交信息

点击相应提交左边的小圆球,能看到具体的错误信息

使用下述命令来查看上次提交的hash号,revert刚才引起流水线故障的提交

代码语言:javascript复制
git log
git revert 131f54ebb5554aef43fc823d5d8d6fb7aaa8898c
git push

revert并且push,1分钟后,流水线自动构建,故障消失

revert并且push,1分钟后,流水线自动构建,故障消失

在流水线上引入一个自动化单元测试失败,并revert来解决问题

现在操练一下当流水线遇到测试失败时,会报什么错

在测试代码中,将断言中的 firstName [4] 改为 firstName [40],然后push代码到代码库

AdminServiceTest.java.

代码语言:javascript复制
class AdminServiceTest {
    @Test
    public void should_retrieve_an_admin_with_correct_names() {
        AdminService adminService = new AdminService();

        Admin admin = adminService.retrieveAdmin(4);

        BDDAssertions.then(admin.getFirstName()).isEqualTo("firstName [40]");
        BDDAssertions.then(admin.getLastName()).isEqualTo("lastName [4]");
    }

}

等1分钟后,流水线被轮询程序自动触发。把鼠标放到有提交的出错构建处,能看到导致这次构建失败的提交人和提交信息。点击相应提交左边的小圆球,能看到具体的错误信息

等1分钟后,流水线被轮询程序自动触发。把鼠标放到有提交的出错构建处,能看到导致这次构建失败的提交人和提交信息

点击相应提交左边的小圆球,能看到具体的错误信息

可以使用上面提到的命令来查看上次提交的hash号,revert刚才引起流水线故障的提交

将Jenkinsfile中的cron改为不那么频繁地构建

每分钟构建一次十分耗费资源,所以可以把轮询次数改为工作时间每2小时构建一次

Jenkinsfile.

代码语言:javascript复制
pipeline {
    agent any

    triggers {
        cron('H H(8-15)/2 * * 1-5')
    }

push代码,1分钟后自动构建,Jenkins会把修改后的轮询配置自动更新到配置页面

作业

操练到此结束。现在该轮到你操练了。可以换一个业务场景操练一下。比如可以将根据id号获取管理员的业务场景,换成根据id号获取学生,从头到尾操练一遍。愿你有所收获

反馈

为了让下次DevOps编程操练让你更有收获,不妨花2分钟填写4个问题

0 人点赞