一、为什么是jenkinsfile
上文我们说了pipeline,已为本文铺路不少,接下里就是将之串联起来。
先想说下,为什么是jenkinsfile, 因为jenkins job还支持pipeline方式。
这种方式,不建议实际使用,仅限于测试或调试groovy代码。
下面贴出来,我们的使用方式。好处是:采用分布式的思想,改动git上的jenkinsfile,就可以让所有的job更新。
二、jenkinsfile的参数
这也有两种方式,我反而又不建议你写在jenkinsfile里。
- Job (建议方式,因为每个工程的参数不一样,需要持久化保存)
- jenkinsfile
对于java语言来说, 我梳理了以下几个必须参数:
- jarName
- port
- projectName(消息通知)
- codeUrl
- branch
为了一些额外需求,我们还定义了几个扩展参数:
- packageType(打包方式,mvn或者gradle。默认为mvn)
- robotKey(消息通知的机器人robot,默认为空)
- childModule(子模块, 用于一个多module的项目打包,指定某模块以发布。默认为空)
三、groovy编码的几点建议
1、变量一定要使用trim()方法
举例,shell命令读取程序版本号,返回值如果没有使用trim(),会额外多一个换行符。-- 后期在使用该变量拼接的时候,很容易就把明明一行执行的,硬生生地被拆成了两行执行。
// 读取java应用的版本号
def getAppVersion(packagePath) {
def appVersion = sh returnStdout: true,
script: """
grep 'git.build.version' ${packagePath}/classes/git.properties | cut -d'=' -f2 | sed 's/\\r//'
"""
// trim()
return appVersion.trim()
}
// 镜像版本号
def dockerImageVersion = appVersion + "-001"
docker build -f ${dockerfileName} -t ${repoProject}/${appName}:${dockerImageVersion} .
// 如果你没有使用trim()的话, 上面的这行代码,就会变成:
docker build -f ${dockerfileName} -t ${repoProject}/${appName}:${dockerImageVersion}
// 我还在纳闷,怎么最后的.被谁吃掉了呢。。。
// 当你期望的是
docker build -f /opt/Dockerfile -t xxx/devops-service:1.0.7-001 .
// 实际却是:
docker build -f /opt/Dockerfile -t xxx/devops-service:1.0.7
2、引入pipeline library
这里的名称,对应前文配置中的name
为了避免jenkinsfile的代码量过于复杂,我们往往会抽取出公共的方法。
@Library('jenkinslib') _
def tools = new com.xhtech.devops.tools()
tools.PrintMes("pull code!!!", "green")
3、读取参数
这里的参数分为两种,一是自定义,一是系统自带。
- 字符串类型的变量,后面都紧跟着trim()方法。
// JOB_NAME是系统自带
String jobName = "${env.JOB_NAME}".trim()
// jarName是我们在job的参数化构建中配置
String jarName = "${env.jarName}".trim()
4、运行shell命令
// 正确的写法,必须由script括起来
script {
sh "mvn clean package -Dmaven.test.skip=true -U"
}
5、切换目录
dir("${env.WORKSPACE}")
6、指定容器
默认容器是jnlp, 如果你的Pod配置了多个容器,docker image的相关操作就必须在docker容器里执行。
- container(‘docker’),这里的docker就是对应PodTemplate中的容器名称。
container('docker') {
dir("${env.WORKSPACE}") {
docker.buildAndPushImage(jarName, port, dockerImageVersion, packagePath, dockerfileName)
}
}
7、归档archiveArtifacts
把版本号写入到文件,并且归档
dir("${env.WORKSPACE}") {
sh "echo ${appVersion} > version_${appVersion}.txt"
archiveArtifacts artifacts: 'version_*.txt', followSymlinks: false
}
8、设置操作的超时时间
timeout(time: 1, unit: "MINUTES") {
// 具体操作,预计在1分钟内完成
}
9、指定工作空间
我们把工作空间持久化到某个目录,而不是放在Pod里。这样的好处是:起到了缓存的作用,不会随着Pod的销毁而需重建。
// 我们会对/opt/.m2进行持久化操作
String sharefile = "/opt/.m2"
String jobName = "${env.JOB_NAME}".trim()
String PROJECT_WORKSPACE = "$sharefile/java-workspaces/$jobName"
agent {
kubernetes {
inheritFrom 'jnlp-maven'
customWorkspace "${PROJECT_WORKSPACE}"
}
}
10、清理空间
安装jenkins plugin: Workspace Cleanup
// 文档地址: https://plugins.jenkins.io/ws-cleanup/
stage('Clean Workspace') {
steps {
cleanWs()
}
}
四、完整的java-k8s.jenkinsfile
#!groovy
@Library('jenkinslib') _
String sharefile = "/opt/.m2"
String dockerfileName = sharefile + "/" + "Dockerfile"
// java代码仓库gitlab的ssh密钥
String gitlabCredentialsId = "ffcbbd41-1e6a-49ec-8277-87a1299606dd"
def tools = new com.xxtech.devops.tools()
def http = new com.xxtech.devops.http()
def docker = new com.xxtech.devops.docker()
def helm = new com.xxtech.devops.helm()
// OA:项目管理--研发项目--产品列表中的英文名称
String projectName = "${env.projectName}".trim()
// jenkins job
String jobName = "${env.JOB_NAME}".trim()
// jar包的名称
String jarName = "${env.jarName}".trim()
// java应用的端口
int port = "${env.port}"
// java代码的git仓库地址
String codeUrl = "${env.codeUrl}".trim()
// java代码的git分支
String branch = "${env.branch}".trim()
// 消息通知的机器人robot
String robotKey = "${env.robotKey}".trim()
// 打包方式,mvn或者gradle
String packageType = "${env.packageType}".trim()
// 构建者
String buildUser = ""
// 版本号, 由jenkins去读取
String appVersion = ""
//docker image version
String dockerImageVersion = ""
// 子模块, 用于一个多module的项目打包,指定某模块以发布
String childModule = "${env.childModule}".trim()
String packagePath = "target"
if (childModule && childModule != "null" && childModule.trim() != "") {
tools.PrintMes("build package for childModule: " + childModule, "green")
packagePath = childModule + "/target"
}
// 判断是否为gradle打包
boolean gradlePackage = packageType.equalsIgnoreCase("gradle")
if (gradlePackage) {
packagePath = "build/libs"
}
String gradleUserHome = "$sharefile/java-gradle-homes"
String gradleProjectCacheDir = "$gradleUserHome/$jobName"
// workspace
String PROJECT_WORKSPACE = "$sharefile/java-workspaces/$jobName"
// 校验不能为空
if (!projectName) {
tools.PrintMes("projectName is null", "red")
return
}
if (!jarName) {
tools.PrintMes("jarName is null", "red")
return
}
if (!codeUrl) {
tools.PrintMes("codeUrl is null", "red")
return
}
if (!branch) {
tools.PrintMes("branch is null", "red")
return
}
//根据jobName读取环境区分
String[] jobInfoArray = jobName.split("_")
// 环境
String buildEnv = jobInfoArray[0]
pipeline {
agent {
kubernetes {
inheritFrom 'jnlp-maven'
customWorkspace "${PROJECT_WORKSPACE}"
}
}
options {
timestamps() //日志会有时间
skipDefaultCheckout() //删除隐式checkout scm语句
disableConcurrentBuilds() //禁止并行
timeout(time: 1, unit: 'HOURS') //流水线超时设置1h
}
stages {
stage('Get UserInfo') {
steps {
wrap([$class: 'BuildUser']) {
script {
def BUILD_USER = "${env.BUILD_USER}"
def BUILD_USER_ID = "${env.BUILD_USER_ID}"
buildUser = BUILD_USER + "(" + BUILD_USER_ID + ")"
}
}
}
}
stage('Pull Code') {
steps {
script {
tools.PrintMes("pull code!!!", "green")
// 拉取代码:git协议
git branch: "$branch", credentialsId: "$gitlabCredentialsId", url: "$codeUrl"
}
}
post {
failure {
//当此Pipeline失败时打印消息
script {
tools.PrintMes("pull code failure!!!", "red")
http.imNotify(projectName, "FAIL", buildEnv, "pull code failure", branch, buildUser, robotKey)
}
}
}
}
stage('Compile') {
steps {
script {
tools.PrintMes("compile!!!", "green")
container('maven') {
dir("${env.WORKSPACE}") {
if (gradlePackage) {
sh "$sharefile/gradle-7.6/bin/gradle build -x test --build-cache -g $gradleUserHome --no-daemon --project-cache-dir $gradleProjectCacheDir"
} else {
sh "mvn clean package -Dmaven.test.skip=true -U -s $sharefile/settings.xml"
}
}
}
}
}
post {
failure {
//当此Pipeline失败时打印消息
script {
tools.PrintMes("compile failure!!!", "red")
http.imNotify(projectName, "FAIL", buildEnv, "compile failure", branch, buildUser, robotKey)
}
}
}
}
stage('Get Version') {
steps {
script {
tools.PrintMes("Get Version!!!", "green")
appVersion = tools.getAppVersion(packagePath)
// 输出程序的版本号
tools.PrintMes("get version: ${appVersion}", "green")
}
}
post {
failure {
//当此Pipeline失败时打印消息
script {
tools.PrintMes("Get Version failure!!!", "red")
http.imNotify(projectName, "FAIL", buildEnv, "Get Version failure", branch, buildUser, robotKey)
}
}
}
}
stage('Push Docker Image') {
steps {
script {
tools.PrintMes("Push Docker Image!!!", "green")
def currentTime = sh returnStdout: true,
script: """
date +%m%d%H%M%S
"""
// 注意:这里的分隔符不能使用冒号, trim()
dockerImageVersion = appVersion + "-" + currentTime.trim()
tools.PrintMes("Docker Image Version: ${dockerImageVersion}", "green")
container('docker') {
dir("${env.WORKSPACE}") {
docker.buildAndPushImage(jarName, port, dockerImageVersion, packagePath, dockerfileName)
}
}
}
}
post {
failure {
//当此Pipeline失败时打印消息
script {
tools.PrintMes("Push Docker Image failure!!!", "red")
http.imNotify(projectName, "FAIL", buildEnv, "Push Docker Image failure", branch, buildUser, robotKey)
}
}
}
}
stage('Clean Workspace') {
steps {
script {
tools.PrintMes("Clean Workspace!!!", "green")
cleanWs()
}
}
post {
failure {
script {
//当此Pipeline失败时打印消息
tools.PrintMes("Clean Workspace failure!!!", "red")
http.imNotify(projectName, "FAIL", buildEnv, "Clean Workspace failure", branch, buildUser, robotKey)
}
}
}
}
stage('Update Helm Yaml') {
steps {
script {
tools.PrintMes("Update Helm Yaml!!!", "green")
helm.updateYaml(jarName, dockerImageVersion)
}
}
post {
failure {
script {
//当此Pipeline失败时打印消息
tools.PrintMes("Update Helm Yaml failure!!!", "red")
http.imNotify(projectName, "FAIL", buildEnv, "Update Helm Yaml failure", branch, buildUser, robotKey)
}
}
}
}
}
post {
success {
//当此Pipeline成功时打印消息
timeout(time: 1, unit: "MINUTES") {
script {
tools.PrintMes("archive jar, send the message of build successfully to user", "green")
container('maven') {
dir("${env.WORKSPACE}") {
sh "echo ${appVersion} > version_${appVersion}.txt"
archiveArtifacts artifacts: 'version_*.txt', followSymlinks: false
}
http.imNotify(projectName, "SUCCESS", buildEnv, "OK", branch, buildUser, robotKey)
}
}
}
}
}
}
五、消息通知
- TODO: 我们对于构建成功的消息内容,增加程序版本号。
具体消息通知的实现,就不在本文的范畴。
写在末尾的话
后期,我们有空,就Jenkins集群、插件(包括二次开发插件)等话题,聊一聊我们的实际使用情况。