Advertisement

企业级DevOps全链路自动化流水线综合解决方案详解

阅读量:

一、Pipeline流水线

1、Pipeline 简介

Pipeline,简单来说,就是一套运行在 Jenkins 上的工作流框架,将原来独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程编排和可视化的工作。

Pipeline官网:Pipeline

使用Pipeline的好处:

  • 代码:Pipeline以代码的形式实现,通常被检入源代码控制,使团队能够编辑,审查和迭代其传送流程。
  • 持久:无论是计划内的还是计划外的服务器重启,Pipeline都是可恢复的。
  • 可停止:Pipeline可接收交互式输入,以确定是否继续执行Pipeline。
  • 多功能:Pipeline支持现实世界中复杂的持续交付要求。它支持fork/join、循环执行,并行执行任务的功能。
  • 可扩展:Pipeline插件支持其DSL的自定义扩展 ,以及与其他插件集成的多个选项。

Jenkins Pipeline 核心概念:

  • Node:节点,一个 Node 就是一个 Jenkins 节点,Master 或者 Agent,是执行 Step 的具体运行环境,比如我们之前动态运行的 Jenkins Slave 就是一个 Node 节点
  • Stage:阶段,一个 Pipeline 可以划分为若干个 Stage,每个 Stage 代表一组操作,比如:Build、Test、Deploy,Stage 是一个逻辑分组的概念,可以跨多个 Node
  • Step:步骤,Step 是最基本的操作单元,可以是打印一句话,也可以是构建一个 Docker 镜像,由各类 Jenkins 插件提供,比如命令:sh 'make',就相当于我们平时 shell 终端中执行 make 命令一样。

Pipeline 脚本是由 Groovy 语言实现的,我们简单了解一下Groovy语法。

2、Groovy 语法

Groovy是一种功能强大,可选类型和动态语言,支持Java平台。

旨在提高开发人员的生产力得益于简洁,熟悉且简单易学的语法。

可以与任何Java程序顺利集成,并立即为您的应用程序提供强大的功能,包括脚本编写功能,特定领域语言编写,运行时和编译时元编程以及函数式编程。

Groovy基本语法如下。

支持命名参数,比如:

复制代码
 def createName(String givenName,String familyName){

    
 return givenName + " " + familyName 
    
 }
    
 // 调用时可以这样
    
 createName familyName = "Lee",givenName = "Bruce"
    
    
    
    
    AI助手

支持默认参数值,比如:

复制代码
 def sayHello(String name = "humans"){

    
     print "hello ${name}"
    
 }
    
 sayHello() //此时括号不能省略
    
    
    
    
    AI助手

支持单引号、双引号。双引号支持插值,单引号不支持。比如:

复制代码
 def name = 'world'

    
 print "hello ${name}" // 结果:hello world
    
 print 'hello ${name}' // 结果:hello ${name}
    
    
    
    
    AI助手

支持三引号。三引号分为三单引号和三双引号。它们都支持换行,区别在于只有三双引号支持插值。比如:

复制代码
 def name = 'world'

    
 def aString = '''line one 
    
 line two 
    
 line three 
    
 ${name}
    
 '''
    
  
    
 def bString = """line one 
    
 line two 
    
 line three 
    
 ${name}
    
 """
    
    
    
    
    AI助手

支持闭包。闭包的定义方法如下:

复制代码
 // 定义闭包

    
 def codeBlock = { print "hello closure" }
    
 //闭包还可以直接被当成函数调用
    
 codeBlock()//结果打印:hello closure
    
    
    
    
    AI助手

还可以将闭包看作一个参数传递给另一个方法:

复制代码
 // 定义一个pipeline函数,它接收一个闭包参数

    
 def pipeline(closure){
    
 closure()
    
 }
    
 // 在调用pipeline函数时,可以这样
    
 pipeline(codeBlock)
    
 // 如果把闭包定义的语句去掉
    
 pipeline({print "hello closure"})
    
 // 由于括号是非必需的,所以
    
 pipeline {
    
     print "hello closure"
    
 }
    
 // 是不是很像Jenkins pipeline
    
    
    
    
    AI助手

闭包的另类用法。我们定义一个stage函数:

复制代码
 println name

    
 closue()
    
 }
    
 // 在正常情况下,我们这样使用stage函数
    
 stage("stage name",{println "closure"})
    
 //最终打印
    
 /**
    
 stage name 
    
 closure 
    
 **/
    
 // 但是,Groovy提供了另一种写法
    
 stage("stage name"){
    
 print "closure"
    
 }
    
    
    
    
    AI助手

1. Groovy 数据类型

1)String 字符串

字符串表示: 单引号、双引号、三引号。

字符串中有变量要使用双引号。

常用方法:

  • contains()是否包含特定内容 返回true false;
  • size()length()字符串数量大小长度;
  • toString()转换成string类型;
  • indexOf()元素的索引;
  • endsWith()是否指定字符结尾;
  • minus() plus() 去掉、增加字符串;
  • reverse() 反向排序;
  • substring(1,2)字符串的指定索引开始的子字符串;
  • toUpperCase() toLowerCase() 字符串大小写转换;
  • split()字符串分割 默认空格分割 返回列表;

2)list 列表

列表符号:[]

常用方法:

  • + - += -= 元素增加减少;
  • isEmpty()判断是否为空;
  • add()添加元素;
  • intersect([2,3]) disjoint([1]) 取交集、判断是否有交集;
  • flatten()合并嵌套的列表;
  • unique()去重;
  • reverse()sort() 反转升序;
  • count()元素个数;
  • join()将元素按照参数链接;
  • sum()min()max()求和 最小值 最大值;
  • contains()包含特定元素;
  • remove(2) removeAll();
  • each{ } 遍历;

3)map 映射

表示:[:]

常用方法:

  • size() map大小;
  • ['key'] .key get() 获取value;
  • isEmpty() 是否为空;
  • containKey()是否包含key;
  • containValue()是否包含指定的value;
  • keySet() 生成key的列表;
  • each{} 遍历map;
  • remove(‘a‘) 删除元素(k-v);
复制代码
 groovy:000>[:]

    
 ==>[:]
    
 groovy:000>[1:2]
    
 ==>[1:2]
    
 groovy:000>[1:2][1]
    
 ==>2
    
 groovy:000>[1:2,3:4,5:6]
    
 ==>[1:2,3:4,5:6]
    
 groovy:000>[1:2,3:4,5:6].keySet()
    
 =>[1,3,5]
    
 groovy:000>[1:2,3:4,5:6].values()
    
 ==>[2,4,6]
    
 groovy:000>[1:2,3:4,5:6] + [7:8]
    
 ==>[1:2, 3:4, 5:6, 7:8]
    
 groovy:000>[1:2,3:4,5:6] - [7:8]
    
 =>[1:2, 3:4, 5:6] 
    
    
    
    
    AI助手

2. Groovy 条件语句

1)if 语句

语法:

复制代码
 if (表达式) {

    
  //XXXX
    
 } else if (表达式2)
    
  //XXXX 
    
 } else
    
  //
    
 }
    
    
    
    
    AI助手
复制代码
 node {

    
     stage('Example') {
    
     if (env.BRANCH_NAME == 'master') {
    
         echo 'I only execute on the master branch'
    
     } else {
    
         echo 'I execute elsewhere'
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

2) switch 语句

复制代码
 switch("${buildType}"){

    
     case "maven":
    
       //xxx
    
       break;
    
     case "ant":
    
       //xxx
    
       break;
    
     default:
    
      //xxxx 
    
 }
    
    
    
    
    AI助手
复制代码
 switch("${buildType}"){

    
     case "maven":
    
       println("This is a maven project !")
    
       break;
    
       ;;
    
     case "ant":
    
       println("This is a ant project !")
    
       break;
    
       ;;
    
     default:
    
      //xxxx 
    
      println("Project Type Error !")
    
 }
    
    
    
    
    AI助手

3. Groovy 循环语句

1)for 循环

复制代码
 test = [1,2,3]

    
 for (i in test){
    
     ///xxx
    
     break;
    
 }
    
    
    
    
    AI助手
复制代码
 langs = ['java','python','groovy']

    
 for (lang in langs){
    
     if (lang == "java"){
    
      println("lang  error in java!")
    
     }else{
    
      println("lang  is ${lang}")
    
     }
    
 }
    
    
    
    
    AI助手

2)while 循环

复制代码
 while(true){

    
     //xxx
    
 }
    
    
    
    
    AI助手

4. Groovy 函数

def定义函数:

复制代码
 def PrintMes(value){

    
     println(value)
    
     //XXXX
    
     return value
    
 }
    
    
    
    
    AI助手
复制代码
 package org.devops

    
  
    
 //格式化输出
    
 def PrintMes(value,color){
    
     colors = ['red'   : "\033[40;31m >>>>>>>>>>>${value}<<<<<<<<<<< \033[0m",
    
           'blue'  : "\033[47;34m ${value} \033[0m",
    
           'green' : "[1;32m>>>>>>>>>>${value}>>>>>>>>>>[m",
    
           'green1' : "\033[40;32m >>>>>>>>>>>${value}<<<<<<<<<<< \033[0m" ]
    
     ansiColor('xterm') {
    
     println(colors[color])
    
     }
    
 }
    
    
    
    
    AI助手

Groovy中的方法调用可以省略括号,比如System.out.println "Hello world"。

5. 异常处理

另一种方法是使用Groovy的异常处理支持来管理脚本化流水线流控制。当步骤失败 ,无论什么原因,它们都会抛出一个异常。处理错误的行为必须使用Groovy中的 try/catch/finally 块 , 例如:

复制代码
 node {

    
     stage('Example') {
    
     try {
    
         sh 'exit 1'
    
     }
    
     catch (exc) {
    
         echo 'Something failed, I should sound the klaxons!'
    
         throw
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

3、创建 Jenkins Pipeline

Pipeline官方教程: https://github.com/jenkinsci/pipeline-examples/pulse

Pipeline 支持两种语法:Declarative(声明式)和 Scripted Pipeline(脚本式)语法。

Pipeline 也有两种创建方法:

  • 可以直接在 Jenkins 的 Web UI 界面中输入脚本。
  • 也可以通过创建一个 Jenkinsfile 脚本文件放入项目源码库中(一般我们都推荐在 Jenkins 中直接从源代码控制(SCM)中直接载入 Jenkinsfile Pipeline 这种方法)。

1. Declarative(声明式)语法

声明式语法更结构化。

声明式语法:

复制代码
 pipeline {

    
     agent any
    
  
    
     //stages:代表整个流水线的所有执行阶段。通常stages只有1个,里面包含多个stage
    
     stages {
    
     //stage:代表流水线中的某个阶段,可能出现n个。一般分为拉取代码,编译构建,部署等阶段。
    
     stage('Hello') {
    
         //steps:代表一个阶段内需要执行的逻辑。steps里面是shell脚本,git拉取代码,ssh远程发布等任意内容。
    
         steps {
    
             echo 'Hello World'
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

一个简单声明式Pipeline:

复制代码
 def getversion(){

    
     def version = '1.0.0'
    
     return version;
    
 }
    
 pipeline{
    
     agent any
    
  
    
     tools{
    
     maven 'MAVEN_HOME'
    
     }
    
     options {
    
     
    
     }
    
     environment{{
    
     
    
     }
    
     parameters {
    
     
    
     }
    
     triggers{
    
     
    
     }
    
     stages {
    
     stage('拉取代码') {
    
         steps {
    
             echo '拉取代码'
    
         }
    
     }
    
     stage('编译构建') {
    
         steps {
    
             echo '编译构建'
    
         }
    
     }
    
     stage('项目部署') {
    
         steps {
    
             echo '项目部署'
    
         }
    
     }
    
     }
    
   41.     post {
    
     always {
    
         
    
     }
    
     success {
    
         
    
     }
    
     failure{
    
         
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

构建后你可以看到整个流程。

常用的Pipeline模板:

gitlab服务器保存公钥信息。

生成秘钥:

复制代码
    ssh-keygen -t rsa 
    
    AI助手

查看公钥信息:

复制代码
    cat /root/.ssh/id_rsa.pub 
    
    AI助手

gitlabe服务器配置:

复制代码
    当前用户->setting->SSH Key->点击 add key按钮
    
    AI助手

配置凭据:

jenkins工作台->系统管理->凭据管理(manager credentials)。

复制代码
 pipeline {

    
     agent any
    
  
    
     stages {
    
     stage('拉取代码') {
    
         steps {
    
            checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '68f2087f-a034-4d39-a9ff-1f776dd3dfa8', url: 'git@192.168.198.152/lagou/jenkinsdemo.git']]])
    
         }
    
     }
    
     stage('编译打包') {
    
         steps {
    
            sh label: '', script: 'mvn clean package'
    
         }
    
     }
    
     stage('项目部署') {
    
         steps {
    
            deploy adapters: [tomcat8(credentialsId: 'afc43e5e-4a4e-4de6-984f- b1d5a254e434', path: '', url: 'http://192.168.66.102:8080')], contextPath: null, war: 'target/*.war'
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

http方式:

复制代码
         stage('拉取代码') {

    
              steps { 
    
                  echo 'gitlab拉取代码' 
    
                  checkout([$class: 'GitSCM', branches: [[name: '*/master']], 
    
                  doGenerateSubmoduleConfigurations: false, extensions: [], 
    
                  submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'b26bd84e-e0cb-4b90-8469-1c2a46213466', 
    
                  url: 'http://192.168.198.152/lagou/jenkinsdemo.git']]]) 
    
              }
    
          }
    
    
    
    
    AI助手

ssh方式:

凭据管理中保存gitlab服务器的私钥信息,类型:SSH Username with private key。

复制代码
 stage('拉取代码') {

    
                steps { 
    
                    echo 'gitlab拉取代码'
    
                    checkout([$class: 'GitSCM', branches: [[name: '*/master']], 
    
 doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], 
    
 userRemoteConfigs: [[credentialsId: 'c8634952-4993-4455-b164-35427823144f', url: 
    
 'ssh://git@192.168.198.152:222/lagou/jenkinsdemo.git']]]) 
    
                    } 
    
            }
    
    
    
    
    AI助手

2. Scripted Pipeline(脚本式)语法

使用Groovy语法实现pipeline,脚本式语法比较灵活和方便扩展,但是需要熟悉groovy语法。

脚本式语法:

复制代码
 node{

    
     stage("编译打包"){
    
     // groovy语法
    
     try{
    
         
    
     }
    
     catch(err){
    
         // 异常处理代码
    
     }
    
     }
    
     stage("部署"){
    
  
    
     }
    
 }
    
    
    
    
    AI助手

脚本式Pipeline案例:

复制代码
 //node:节点,一个 node 就是一个 Jenkins 节点,Master 或者 Agent,

    
 //      是执行 Step 的具体运行环境,后续讲到Jenkins的Master-Slave架构的时候用到。
    
 node {
    
     def mvnHome
    
     //stage:阶段,一个 Pipeline 可以划分为若干个 Stage,每个 Stage 代表一组操作,
    
     //       比如:Build、Test、Deploy,Stage 是一个逻辑分组的概念。
    
     stage('Preparation') {
    
         //step:步骤,step 是最基本的操作单元,可以是打印一句话,也可以是构建一个 Docker 镜像,
    
         //      由各类 Jenkins 插件提供,比如命令:sh 'make',就相当于我们平时 shell 终端中执行 make 命令一样。
    
     // Get some code from a GitHub repository
    
     git 'https://github.com/jglick/simple-maven-project-with-tests.git'
    
     // Get the Maven tool.
    
     // ** NOTE: This 'M3' Maven tool must be configured
    
     // **       in the global configuration.
    
     mvnHome = tool 'M3'
    
     }
    
     stage('Build') {
    
     // Run the maven build
    
     withEnv(["MVN_HOME=$mvnHome"]) {
    
         if (isUnix()) {
    
             sh '"$MVN_HOME/bin/mvn" -Dmaven.test.failure.ignore clean package'
    
         } else {
    
             bat(/"%MVN_HOME%\bin\mvn" -Dmaven.test.failure.ignore clean package/)
    
         }
    
     }
    
     }
    
     stage('Results') {
    
     junit '**/target/surefire-reports/TEST-*.xml'
    
     archiveArtifacts 'target/*.jar'
    
     }
    
 }
    
    
    
    
    AI助手

3. Declarative pipeline和Scripted pipeline的比较

早期的 pipeline plugin 只支持脚本式语法,声明式语法是在 pipeline2.5 之后新增加的,相对而言,声明式语法更简单,即使没有 groovy 语言的基础也能进行基本的操作。Jenkins 社区的动向也是偏向于声明式语法,所以以声明式语法为例进行说明。

共同点:

两者都是pipeline代码的持久实现,都能够使用pipeline内置的插件或者插件提供的steps,两者都可以利用共享库扩展。

区别:

两者不同之处在于语法和灵活性。Declarative pipeline对用户来说,语法更严格,有固定的组织结构,更容易生成代码段,使其成为用户更理想的选择。但是Scripted pipeline更加灵活,因为Groovy本身只能对结构和语法进行限制,对于更复杂的pipeline来说,用户可以根据自己的业务进行灵活的实现和扩展。

二、Pipeline流水线快速入门

1、流水线项目构建基本流程

1. 安装插件 Pipeline

初始化jenkins环境时已经默认安装了pipeline插件。

也可以自己安装 Pipeline 和 Pipeline Maven Integration 等插件。

2. 新建任务

新建任务 -> 输入一个任务名称【web-demo-pipeline】 -> 流水线。

3. 脚本编写

直接在Jenkins的WEB UI上输入脚本。

脚本内容:

复制代码
 node {

    
   stage('Clone') {
    
     echo "1.Clone Stage"
    
   }
    
   stage('Test') {
    
     echo "2.Test Stage"
    
   }
    
   stage('Build') {
    
     echo "3.Build Stage"
    
   }
    
   stage('Deploy') {
    
     echo "4. Deploy Stage"
    
   }
    
 }
    
    
    
    
    AI助手

然后保存--> 点击构建--> 观察日志。

输出符合我们脚本内容。

4. 在slave中运行Pipeline

上面对Jenkins的Pipeline做了简单的测试,但是其并未在我们的Slave中运行,如果要在Slave中运行,其就要使用我们前面添加的Label,如下:

复制代码
 node('joker-jnlp') {

    
     stage('Clone') {
    
       echo "1.Clone Stage"
    
     }
    
     stage('Test') {
    
       echo "2.Test Stage"
    
     }
    
     stage('Build') {
    
       echo "3.Build Stage"
    
     }
    
     stage('Deploy') {
    
       echo "4. Deploy Stage"
    
     }
    
 }
    
    
    
    
    AI助手

然后我们保存并点击构建,观察Pod的变化:

复制代码
 [root@master ~]# kubectl get pod -n devops  -w

    
 NAME                      READY   STATUS    RESTARTS   AGE
    
 jenkins-6595ddd5d-m5fvd   1/1     Running   0          2d23h
    
 jenkins-slave-vq8wf       0/1     Pending   0          0s
    
 jenkins-slave-vq8wf       0/1     Pending   0          0s
    
 jenkins-slave-vq8wf       0/1     ContainerCreating   0          0s
    
 jenkins-slave-vq8wf       1/1     Running             0          2s
    
 jenkins-slave-vq8wf       1/1     Terminating         0          27s
    
 jenkins-slave-vq8wf       1/1     Terminating         0          27s
    
    
    
    
    AI助手

我们可以看到其依据我们定义的模板动态生成了jenkins-slave的Pod,我们在Jenkins的日志中查看:

可以看到两个的名字是一样的。

2、Pipeline部署完整应用

部署应用的流程如下:

  • 编写代码
  • 测试
  • 编写 Dockerfile
  • 构建打包 Docker 镜像
  • 推送 Docker 镜像到仓库
  • 编写 Kubernetes YAML 文件
  • 更改 YAML 文件中 Docker 镜像 TAG
  • 利用 kubectl 工具部署应用

基本的Pipeline脚本框架如下:

复制代码
 node('joker-jnlp') {

    
     stage('Clone') {
    
       echo "1.Clone Stage"
    
     }
    
     stage('Test') {
    
       echo "2.Test Stage"
    
     }
    
     stage('Build') {
    
       echo "3.Build Docker Image Stage"
    
     }
    
     stage('Push') {
    
       echo "4.Push Docker Image Stage"
    
     }
    
     stage('YAML') {
    
       echo "5. Change YAML File Stage"
    
     }
    
     stage('Deploy') {
    
       echo "6. Deploy Stage"
    
     }
    
 }
    
    
    
    
    AI助手

第一步:克隆代码。

复制代码
 stage('Clone') {

    
     echo "1.Clone Stage"
    
     git url: "https://github.com/baidjay/jenkins-demo.git"
    
     script {
    
     build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
    
     }
    
     echo "${build_tag}"
    
 }
    
    
    
    
    AI助手

我们这里采用和git commit的记录为镜像的 tag,这里有一个好处就是镜像的 tag 可以和 git 提交记录对应起来,也方便日后对应查看。但是由于这个 tag 不只是我们这一个 stage 需要使用,下一个推送镜像是不是也需要,所以这里我们把这个 tag 编写成一个公共的参数,把它放在 Clone 这个 stage 中。

第二步:测试。

复制代码
 stage('Test') {

    
       echo "2.Test Stage"
    
     }
    
    
    
    
    AI助手

测试可以是单测,也可以是工具,我们这里就简单存在这个步骤。

第三步:构建镜像。

复制代码
 stage('Build') {

    
     echo "3.Build Docker Image Stage"
    
     sh "docker build -t registry.cn-hangzhou.aliyuncs.com/ik9s/jenkins-demo:${build_tag} ."
    
 }
    
    
    
    
    AI助手

这一步我们就使用到上面定义的build_tag变量。

第四步:推送镜像。

复制代码
 stage('Push') {

    
     echo "4.Push Docker Image Stage"
    
     sh "docker login --username=www.22222@qq.com registry.cn-hangzhou.aliyuncs.com -p xxxxx"
    
     sh "docker push registry.cn-hangzhou.aliyuncs.com/ik9s/jenkins-demo:${build_tag}"
    
 }
    
    
    
    
    AI助手

配置Jenkins,隐藏用户名密码信息:

其中ID:AliRegistry 是我们后面要用的值。

这样我们上面的脚本就可以定义如下:

复制代码
 stage('Push') {

    
     echo "4.Push Docker Image Stage"
    
     withCredentials([usernamePassword(credentialsId: 'AliRegistry', passwordVariable: 'AliRegistryPassword', usernameVariable: 'AliRegistryUser')]) {
    
     sh "docker login -u ${AliRegistryUser} registry.cn-hangzhou.aliyuncs.com -p ${AliRegistryPassword}"
    
     sh "docker push registry.cn-hangzhou.aliyuncs.com/ik9s/jenkins-demo:${build_tag}"
    
     }
    
 }
    
    
    
    
    AI助手

注意我们这里在 stage 中使用了一个新的函数withCredentials,其中有一个 credentialsId 值就是我们刚刚创建的 ID 值,而对应的用户名变量就是 ID 值加上 User,密码变量就是 ID 值加上 Password,然后我们就可以在脚本中直接使用这里两个变量值来直接替换掉之前的登录 docker hub 的用户名和密码,现在是不是就很安全了,我只是传递进去了两个变量而已,别人并不知道我的真正用户名和密码,只有我们自己的 Jenkins 平台上添加的才知道。

第五步:更改YAML文件。

复制代码
 stage('YAML') {

    
     echo "5. Change YAML File Stage"
    
     sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
    
     sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml"
    
 }
    
    
    
    
    AI助手

其YAML文件为(YAML文件放在项目根目录):

复制代码
 apiVersion: extensions/v1beta1

    
 kind: Deployment
    
 metadata:
    
   name: jenkins-demo
    
 spec:
    
   template:
    
     metadata:
    
       labels:
    
     app: jenkins-demo
    
     spec:
    
       containers:
    
       - image: registry.cn-hangzhou.aliyuncs.com/ik9s/jenkins-demo:<BUILD_TAG>
    
     imagePullPolicy: IfNotPresent
    
     name: jenkins-demo
    
     env:
    
     - name: branch
    
       value: <BRANCH_NAME>
    
    
    
    
    AI助手

第六步:部署。

部署阶段我们增加人工干预,可能需要将该版本先发布到测试环境、QA 环境、或者预览环境之类的,总之直接就发布到线上环境去还是挺少见的,所以我们需要增加人工确认的环节,一般都是在 CD 的环节才需要人工干预,比如我们这里的最后两步,我们就可以在前面加上确认,比如:
我们将YAML这一步改为:

复制代码
 stage('YAML') {

    
     echo "5. Change YAML File Stage"
    
     def userInput = input(
    
     id: 'userInput',
    
     message: 'Choose a deploy environment',
    
     parameters: [
    
         [
    
             $class: 'ChoiceParameterDefinition',
    
             choices: "Dev\nQA\nProd",
    
             name: 'Env'
    
         ]
    
     ]
    
     )
    
     echo "This is a deploy step to ${userInput.Env}"
    
     sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
    
     sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml"
    
     sh "sed -i 's#cnych/jenkins-demo#registry.cn-hangzhou.aliyuncs.com/ik9s/jenkins-demo#' k8s.yaml"
    
 }
    
    
    
    
    AI助手

然后再部署阶段:

复制代码
 stage('Deploy') {

    
     echo "6. Deploy Stage"
    
     if (userInput.Env == "Dev") {
    
       // deploy dev stuff
    
     } else if (userInput.Env == "QA"){
    
       // deploy qa stuff
    
     } else {
    
       // deploy prod stuff
    
     }
    
     sh "kubectl apply -f k8s.yaml"
    
 }
    
    
    
    
    AI助手

由于这一步也属于部署的范畴,所以我们可以将最后两步都合并成一步,我们最终的Pipeline脚本如下:

复制代码
 node('joker-jnlp') {

    
     stage('Clone') {
    
     echo "1.Clone Stage"
    
     git url: "https://github.com/baidjay/jenkins-demo.git"
    
     script {
    
     build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
    
     }
    
     echo "${build_tag}"
    
     }
    
     stage('Test') {
    
       echo "2.Test Stage"
    
     }
    
     stage('Build') {
    
     echo "3.Build Docker Image Stage"
    
     sh "docker build -t registry.cn-hangzhou.aliyuncs.com/ik9s/jenkins-demo:${build_tag} ."
    
     }
    
     stage('Push') {
    
     echo "4.Push Docker Image Stage"
    
     withCredentials([usernamePassword(credentialsId: 'AliRegistry', passwordVariable: 'AliRegistryPassword', usernameVariable: 'AliRegistryUser')]) {
    
     sh "docker login -u ${AliRegistryUser} registry.cn-hangzhou.aliyuncs.com -p ${AliRegistryPassword}"
    
     sh "docker push registry.cn-hangzhou.aliyuncs.com/ik9s/jenkins-demo:${build_tag}"
    
     }
    
     }
    
     stage('Deploy') {
    
     echo "5. Change YAML File Stage"
    
     def userInput = input(
    
     id: 'userInput',
    
     message: 'Choose a deploy environment',
    
     parameters: [
    
         [
    
             $class: 'ChoiceParameterDefinition',
    
             choices: "Dev\nQA\nProd",
    
             name: 'Env'
    
         ]
    
     ]
    
     )
    
     echo "This is a deploy step to ${userInput}"
    
     sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
    
     sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml"
    
     echo "6. Deploy Stage"
    
     if (userInput == "Dev") {
    
       // deploy dev stuff
    
     } else if (userInput == "QA"){
    
       // deploy qa stuff
    
     } else {
    
       // deploy prod stuff
    
     }
    
     sh "kubectl apply -f k8s.yaml -n default"
    
     }
    
 }
    
    
    
    
    AI助手

然后构建面板如下:

然后查看Pod日志如下:

复制代码
 [root@master jenkins]# kubectl logs jenkins-demo-789fdc6878-5pzbx

    
 Hello, Kubernetes!I'm from Jenkins CI!
    
    
    
    
    AI助手

3、Pipeline Script from SCM

前面我们都是直接在Jenkins的UI界面编写Pipeline代码,只是完成了一次手动的添加任务的构建过程,这样不方便脚本维护,在实际的工作实践中,建议把Pipeline脚本写入到 Jenkinsfile 文件中,然后放在项目中和代码一起提交到代码仓库中进行版本管理。

项目中编写脚本:

现在我们将上面的 Pipeline 脚本拷贝到一个 Jenkinsfile 中,将该文件放入上面的 git 仓库中,但是要注意的是,现在既然我们已经在 git 仓库中了,是不是就不需要 git clone 这一步骤了,所以我们需要将第一步 Clone 操作中的 git clone 这一步去掉。

复制代码
 node('joker-jnlp') {

    
     stage('Prepare') {
    
     echo "1.Prepare Stage"
    
     script {
    
     build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
    
     }
    
     echo "${build_tag}"
    
     }
    
     stage('Test') {
    
       echo "2.Test Stage"
    
     }
    
     stage('Build') {
    
     echo "3.Build Docker Image Stage"
    
     sh "docker build -t registry.cn-hangzhou.aliyuncs.com/ik9s/jenkins-demo:${build_tag} ."
    
     }
    
     stage('Push') {
    
     echo "4.Push Docker Image Stage"
    
     withCredentials([usernamePassword(credentialsId: 'AliRegistry', passwordVariable: 'AliRegistryPassword', usernameVariable: 'AliRegistryUser')]) {
    
     sh "docker login -u ${AliRegistryUser} registry.cn-hangzhou.aliyuncs.com -p ${AliRegistryPassword}"
    
     sh "docker push registry.cn-hangzhou.aliyuncs.com/ik9s/jenkins-demo:${build_tag}"
    
     }
    
     }
    
     stage('Deploy') {
    
     echo "5. Change YAML File Stage"
    
     def userInput = input(
    
     id: 'userInput',
    
     message: 'Choose a deploy environment',
    
     parameters: [
    
         [
    
             $class: 'ChoiceParameterDefinition',
    
             choices: "Dev\nQA\nProd",
    
             name: 'Env'
    
         ]
    
     ]
    
     )
    
     echo "This is a deploy step to ${userInput}"
    
     sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
    
     sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml"
    
     echo "6. Deploy Stage"
    
     if (userInput == "Dev") {
    
       // deploy dev stuff
    
     } else if (userInput == "QA"){
    
       // deploy qa stuff
    
     } else {
    
       // deploy prod stuff
    
     }
    
     sh "kubectl apply -f k8s.yaml -n default"
    
     }
    
 }
    
    
    
    
    AI助手

然后我们更改上面的 jenkins-demo 这个任务,点击 Configure -> 最下方的 Pipeline 区域 -> 将之前的 Pipeline Script 更改成 Pipeline Script from SCM,然后根据我们的实际情况填写上对应的仓库配置,要注意 Jenkinsfile 脚本路径。

在实际的项目中,往往一个代码仓库都会有很多分支的,比如开发、测试、线上这些分支都是分开的,一般情况下开发或者测试的分支我们希望提交代码后就直接进行 CI/CD 操作,而线上的话最好增加一个人工干预的步骤,这就需要 Jenkins 对代码仓库有多分支的支持,当然这个特性是被 Jenkins 支持的。

然后新建一个 Jenkinsfile 文件,配置如下:

复制代码
 node('joker-jnlp') {

    
     stage('Prepare') {
    
     echo "1.Prepare Stage"
    
     checkout scm
    
     script {
    
         build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
    
         if (env.BRANCH_NAME != 'master') {
    
             build_tag = "${env.BRANCH_NAME}-${build_tag}"
    
         }
    
     }
    
     }
    
     stage('Test') {
    
       echo "2.Test Stage"
    
     }
    
     stage('Build') {
    
     echo "3.Build Docker Image Stage"
    
     sh "docker build -t registry.cn-hangzhou.aliyuncs.com/ik9s/jenkins-demo:${build_tag} ."
    
     }
    
     stage('Push') {
    
     echo "4.Push Docker Image Stage"
    
     withCredentials([usernamePassword(credentialsId: 'AliRegistry', passwordVariable: 'AliRegistryPassword', usernameVariable: 'AliRegistryUser')]) {
    
         sh "docker login -u ${AliRegistryUser} registry.cn-hangzhou.aliyuncs.com -p ${AliRegistryPassword}"
    
         sh "docker push registry.cn-hangzhou.aliyuncs.com/ik9s/jenkins-demo:${build_tag}"
    
     }
    
     }
    
     stage('Deploy') {
    
     echo "5. Deploy Stage"
    
     if (env.BRANCH_NAME == 'master') {
    
         input "确认要部署线上环境吗?"
    
     }
    
     sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
    
     sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml"
    
     sh "kubectl apply -f k8s.yaml --record"
    
     }
    
 }
    
    
    
    
    AI助手

在第一步中我们增加了checkout scm 命令,用来检出代码仓库中当前分支的代码,为了避免各个环境的镜像 tag 产生冲突,我们为非 master 分支的代码构建的镜像增加了一个分支的前缀,在第五步中如果是 master 分支的话我们才增加一个确认部署的流程,其他分支都自动部署,并且还需要替换 k8s.yaml 文件中的环境变量的值。

三、Pipeline 语法详解

Pipeline插件的集成语法参考:https://www.jenkins.io/doc/pipeline/steps/

Jenkins 的工作流程可以简单概括为 build-deploy-test-release,每个流程之间我们都可以用 Pipeline 来连接,大致如下效果图:

Jenkins pipeline 是基于 Groovy 语言实现的一种 DSL(Domain-Specific Language,领域特定语言),可以理解为适用于 Jenkins 的编程语言。Pipeline用于描述流水线如何进行,包括编译、打包、部署、测试等等步骤。

脚本式语法:

使用Groovy语法实现pipeline,脚本式语法比较灵活和方便扩展,但是需要熟悉groovy语法。

复制代码
 node{

    
     stage("编译打包"){
    
     // groovy语法
    
     try{
    
         
    
     }
    
     catch(err){
    
         // 异常处理代码
    
     }
    
     }
    
     stage("部署"){
    
  
    
     }
    
 }
    
    
    
    
    AI助手

声明式语法 :

复制代码
 def getversion(){

    
     def version = '1.0.0'
    
     return version;
    
 }
    
 pipeline{
    
     agent any
    
  
    
     tools{
    
     maven 'MAVEN_HOME'
    
     }
    
     options {
    
     
    
     }
    
     environment{{
    
     
    
     }
    
     parameters {
    
     
    
     }
    
     triggers{
    
     
    
     }
    
     stages {
    
     stage('编译打包') {
    
         environment {
    
             DEBUG_FLAGS = '-g'
    
         }
    
         echo '编译打包'
    
     }
    
     stage('部署') {
    
         steps {
    
             echo '部署'
    
         }
    
     }
    
     }
    
   37.     post {
    
     always {
    
         
    
     }
    
     success {
    
         
    
     }
    
     failure{
    
         
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

声明式语法更简单和结构化,具体的含义如下:

  • pipeline :后面用一对 {} 也就是闭包,表示整条流水线,里面是流水线中具体的处理流程。
  • agent :用来指定整个流水线或者某一个阶段在哪个机器节点上执行。如果是 any 的话表示该流水线或者阶段可以运行在任意一个定义好的节点上。这个指令必须要有,而且一般会放在顶层 pipeline{...} 的下一层,在 stage{...} 中也可以使用 agent,但是一般不这么用。
  • stages :后面跟一对 {},类似于一个容器,封装了一个或多个 stage,也就是将不同的阶段组织在一起;例如 build 是一个 stage, test 是第二个 stage,deploy 是第三个 stage。通过 stage 隔离,让 Pipeline 代码读写非常直观。
  • stage :后面跟一对 {},流水线中的某个阶段,其中封装了多个执行步骤,每个阶段都需要有个名称。
  • steps :封装了在一个阶段中的一个或多个具体的执行步骤。在本例中 echo 就是一个步骤。

接下来我们一一介绍上面提到的 pipeline 中包含的最基本的几个 section,以及另外一些可选的 section。

1、agent

用来指定 pipeline 的执行节点,一般放在顶层的 pipeline 中。agent 部分支持几种不同的参数以此来适应不同的应用场景。

1. any

作用:表示可以在任意的节点或者代理上执行此 pipeline。

代码示例:

复制代码
 pipeline {

    
   agent any
    
 }
    
    
    
    
    AI助手

2. none

作用:在 pipeline 的顶层应用中使用此参数的话表示不会为整个 pipeline 指定执行的节点,需要在每个 stage 部分用 pipeline 指定执行的节点。

代码示例:

复制代码
 pipeline {

    
     agent none
    
     stages {
    
     stage('test'){
    
         agent {
    
             label '具体的节点名称'
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

3. label

作用:在标签指定的可用代理上执行 pipeline 或 stage,比如 agent {label "label"} 表示流水线或者阶段可以运行在任何一个具有 label 标签的代理节点上。

代码示例:

复制代码
 pipeline {

    
     agent {
    
     label '具体的一个节点 label 名称'
    
     }
    
 }
    
    
    
    
    AI助手

4. 自定义工作空间

作用:代理节点的标签新增了一个特性,允许为流水线或阶段指定一个自定义的工作空间,用 customWorkspace 指令来指定,和 label 功能类似。

代码示例:

复制代码
 pipeline {

    
     agent {
    
     node {
    
         label "xxx-agent-机器"
    
         customWorkspace "${env.JOB_NAME}/${env.BUILD_NUMBER}"
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

此处的 node 可以换成 label,但是为了避免 Docker 代理节点中的 label 用法混淆,一般用 node 表示。这种类型的 agent 在实际工作中的使用场景是最多的。

测试代码:

复制代码
 pipeline {

    
     agent {
    
     node {
    
         label "xxx-agent-机器"
    
         customWorkspace "${env.JOB_NAME}/${env.BUILD_NUMBER}"
    
     }
    
     } 
    
     stages {
    
     stage ("Build") {
    
         bat "dir"  //执行windows下的bat命令
    
     } 
    
     stage ('test') {
    
         echo ${JAVA_HOME}  //打印JAVA_HOME
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

可以将以上代码段放到 Jenkinsfile 中或者在 Jenkins ui 中去执行。

2、POST

post 部分用来指定 pipeline 或者 stage 执行完毕后的一些操作,比如发送邮件、环境清理等。post 部分在 Jenkins 代码中是可选的,可以放到顶层,也就是和 agent 同级,也可以放到stage 中。

在 post 代码块中支持多种指令,比如:always、success、failure、aborted、unstable、changed 等等。

post支持的条件块:

  • always: 无论stage的执行结果如何,此块中的预置操作都会执行。
  • changed:只有当stage的执行后,当前状态与之前发生了改变时,此块中的预置操作才会执行。
  • fixed:上一次运行为不稳定或者失败状态,本次运行成功时,此块中的预置操作才会执行。
  • regression:上一次运行成功,本次运行为失败、不稳定、中止状态时,此块中的预置操作才会执行。
  • aborted:当手动中止运行时,此块中的预置操作才会执行。
  • failure:当stage的状态为失败时,此块中的预置操作才会执行。
  • success:当stage的状态为成功时,此块中的预置操作才会执行。
  • unstable: 当前stage的状态为不稳定时,此块中的预置操作才会执行。
  • unsuccessful:当前stage的状态不是成功时,此块中的预置操作才会执行。
  • cleanup:无论stage的状态为何种状态,在post中的其他的条件预置操作执行之后,此块中的预置操作就会执行。

1. always

作用:当 pipeline 执行完毕后一定会执行的操作,不管成功还是还失败。比如说文件句柄的关闭或者数据库的清理工作等。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     stages {
    
     stage ("Build") {
    
         bat "dir"
    
     }
    
     } 
    
     post {
    
     always {
    
         script {
    
             //写相关清除/恢复环境等操作代码
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

2. success

作用:当 pipeline 执行完毕后且构建结果为成功状态时才会执行的操作。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     stages {
    
     stage ("Build") {
    
         bat "dir"
    
     }
    
     } 
    
     post {
    
     success{
    
         script {
    
         //写相关清除/恢复环境等操作代码
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

3. failure

作用:当 pipeline 执行完毕后且构建结果为失败时执行的操作,比如发送错误日志给相关人员。

4. changed

作用:当 pipeline 执行完毕后且构建状态和之前不一致时执行的操作。

5. aborted

作用:当 pipeline 被手动终止时执行的操作。

6. unstable

作用:当 pipeline 构建结果不稳定时执行的操作。

7. 以上命令的组合

在 post 部分是可以包含多个条件块,也就是以上命令的组合,比如:

复制代码
 pipeline {

    
     agent any
    
     stages {
    
     stage ("Build") {
    
         bat "dir"
    
     }
    
     }
    
     post {
    
     always {
    
         script {
    
             echo "post always "
    
         }
    
     } 
    
     success{
    
         script {
    
             echo "post success"
    
         }
    
     } 
    
     failure{
    
         script {
    
             echo "post failure"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

3、stages/stage/steps

stages**:** Pipeline 中单个阶段的操作封装到 stages 中,stages 中可以包含多个 stage。

stage**:** 一个单独的阶段,实际上所有实际工作都将包含在一个或多个 stage 指令中。stage{…} 里面有一个强制的字符串参数,用来描述这个 stage 的作用,这个字符串参数是不支持变量的,只能你自己取名一个描述字段。

steps**:** 一个 stage 下至少有一个 steps,一般也就是一个 steps。可以在 steps 下写调用一个或者几个方法,也就是两三行代码。

有以下注意点:

  • 在声明式 pipeline 脚本中,有且只有一个 stages。
  • 一个 stage{…} 必须有且只有一个 steps{…}, 或者 parallel{…} 或者 stages {…},多层嵌套只支持在最后一个 stage{…} 里面。
  • 在声明式语法中只支持 steps,不支持在 steps {…} 里面嵌套写 step{…}。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     stages {
    
     stage('build') {
    
         steps { echo 'build' }
    
     }
    
     stage ('test') {
    
         steps { echo "${JAVA_HOME}" }  //打印 JAVA_HOME
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

4、environment

作用:通过键值对的方式定义整个 pipeline 或者 stage 中使用的环境变量。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     environment {
    
     test = true
    
     } 
    
     stages {
    
     stage('build') {
    
         steps {
    
             script{
    
                 if(test == true) {
    
                 // 一些特定的操作
    
                     echo 'sucess'
    
                 }
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

设置环境变量,可在在 pipeline中 或 stage配置:

  • 在 pipeline 中定义 environment, 表示 pipeline 全局使用的环境变量;
  • 在 stage 中定义 environment, 表示当前 stage 的环境变量;

有三种引用方式:

  • ${env.BUILD_NUMBER} 方式一,推荐使用;
  • $env.BUILD_NUMBER 方式二;
  • ${BUILD_NUMBER} 方式三,不推荐使用;

内置的环境变量:

Jenkins 流水线通过全局变量 env 提供环境变量,它在 Jenkinsfile 文件的任何地方都可以使用。Jenkins 流水线中可访问的完整的环境变量列表记录在 ${YOUR_JENKINS_URL}/pipeline-syntax/globals#env,并且包括:

  • BUILD_ID:当前构建的 ID,与 Jenkins 版本 1.597+ 中创建的构建号 BUILD_NUMBER 是完全相同的。
  • BUILD_NUMBER:当前构建号,比如 “153”。
  • BUILD_TAG:字符串 jenkins-${JOB_NAME}-${BUILD_NUMBER}。可以放到源代码、jar 等文件中便于识别。
  • BUILD_URL:可以定位此次构建结果的 URL(比如 http://buildserver/jenkins/job/MyJobName/17/
  • EXECUTOR_NUMBER:用于识别执行当前构建的执行者的唯一编号(在同一台机器的所有执行者中)。这个就是你在“构建执行状态”中看到的编号,只不过编号从 0 开始,而不是 1。
  • JAVA_HOME:如果你的任务配置了使用特定的一个 JDK,那么这个变量就被设置为此 JDK 的 JAVA_HOME。当设置了此变量时,PATH 也将包括 JAVA_HOME 的 bin 子目录。
  • JENKINS_URL:Jenkins 服务器的完整 URL,比如 https://example.com:port/jenkins/ (注意:只有在“系统设置”中设置了 Jenkins URL 才可用)。
  • JOB_NAME:本次构建的项目名称,如 “foo” 或 “foo/bar”。
  • NODE_NAME:运行本次构建的节点名称。对于 master 节点则为 “master”。
  • WORKSPACE:workspace 的绝对路径。

自定义pipeline环境变量:

当pipeline变得复杂时,我们就会有定义自己的环境变量的需求。声明式pipeline提供了environment指令,方便自定义变量。比如:

复制代码
 pipeline {

    
     agent any 
    
     environment {
    
     CC = 'clang'
    
     }
    
     stages {
    
     stage('Example'){
    
         environment {
    
             DEBUG_FLAGS = '-g' 
    
         }
    
     steps {
    
         sh "${CC} ${DEBUG_FLAGS}"
    
         sh "printenv"
    
     }
    
     }
    
     
    
 }
    
    
    
    
    AI助手

另外,environment指令可以在pipeline中定义,代表变量作用域为整个pipeline;也可以在stage中定义,代表变量只在该阶段有效。

但是这些变量都不是跨pipeline的,比如pipeline a访问不到pipeline b的变量。在pipeline之间共享变量可以通过参数化pipeline来实现。

在实际工作中,还会遇到一个环境变量引用另一个环境变量的情况。在environment中可以这样定义:

复制代码
 environment {

    
     __server_name = 'mail-server'
    
     __version="${BUILD_NUMBER}"
    
     __artifact_name = "${__server_name}-${__version}.jar"
    
 }
    
    
    
    
    AI助手

值得注意的是,如果在environment中定义的变量与env中的变量重名,那么被重名的变量的值会被覆盖掉。比如在environment中定义PATH变量(PATH也是env中的一个变量)。

复制代码
 environment {

    
     PATH = "invalid path"
    
 }
    
    
    
    
    AI助手

在执行sh指令时,我们将看到无法在系统上执行。

复制代码
 [Pipeline] sh

    
 [maven-pipeline2] Running shell script ‘sh’: No such file or directory nohup: failed to run command
    
    
    
    
    AI助手

小技巧:为避免变量名冲突,可根据所在公司的实际情况,在变量名前加上前缀,比如__server_name,__就是前缀。

env中的变量都是Jenkins内置的,或者是与具体pipeline相关的。有时候,我们需要定义一些全局的跨pipeline的自定义变量。

进入Manage Jenkins→Configure System→Global properties页,勾选“Environment variables”复选框,单击“Add”按钮,在输入框中输入变量名和变量值即可。

自定义全局环境变量会被加入 env 属性列表中,所以,使用自定义全局环境变量与使用Jenkins内置变量的方法无异:${env.g name}。

全局变量可以在搭建好的jenkins服务上查看,访问地址:http://jenkins访问地址/pipeline-syntax/globals#env

5、options

options 指令在 pipeline 也是可选的。用来指定一些属性和值,这些预定义的选项可以应用到整个流水线中,可以理解为在 Jenkins web 表单里一个项目的基本配置中定义的事情。

可用配置:

  • buildDiscarder:为最近的流水线运行的特定数量保存组件和控制台输出。例如: options { buildDiscarder(logRotator(numToKeepStr: '1')) }
  • disableConcurrentBuilds:不允许同时执行流水线。 可被用来防止同时访问共享资源等。 例如: options { disableConcurrentBuilds() }
  • overrideIndexTriggers:允许覆盖分支索引触发器的默认处理。 如果分支索引触发器在多分支或组织标签中禁用, options { overrideIndexTriggers(true) } 将只允许它们用于促工作。否则, options { overrideIndexTriggers(false) } 只会禁用改作业的分支索引触发器。
  • skipDefaultCheckout:在agent 指令中,跳过从源代码控制中检出代码的默认情况。例如: options { skipDefaultCheckout() }
  • skipStagesAfterUnstable:一旦构建状态变得UNSTABLE,跳过该阶段。例如: options { skipStagesAfterUnstable() }
  • checkoutToSubdirectory:在工作空间的子目录中自动地执行源代码控制检出。例如: options { checkoutToSubdirectory('foo') }
  • timeout:设置流水线运行的超时时间, 在此之后,Jenkins将中止流水线。例如: options { timeout(time: 1, unit: 'HOURS') }
  • retry:在失败时, 重新尝试整个流水线的指定次数。 For example: options { retry(3) }
  • timestamps :预谋所有由流水线生成的控制台输出,与该流水线发出的时间一致。 例如: options { timestamps() }

1. retry

作用:表示 Jenkins 中的 job 执行失败后继续进行几次尝试。可以放到顶层的 pipeline 中也可以放到 stage 中。注意:这个次数是指总次数,包括第 1 次失败。

复制代码
 pipeline {

    
     agent any
    
     options {
    
     retry(3)
    
     }
    
     stages {
    
     stage('test') {
    
         steps {
    
             // 步骤
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

2. buildDiscarder

作用:保留指定数量的流水线执行,包含控制台输出以及制品。当 pipeline 指定完毕后,会在工作空间中保存制品和执行日志,如果执行次数太多的会话,这些内容会占用很多的存储空间,使用该参数后会只保留最近指定次数的构建结果,自动清理之前的内容。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     options {
    
     buildDiscarder(logRotator(numToKeepStr:'3'))
    
     }
    
     stages {
    
     stage('test') {
    
         steps {
    
             // 步骤
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

说明:logRotator 元素并没有什么作用,主要是历史原因。

3. checkoutToSubdirectory

作用:指定检出到工作空间的子目录中。Jenkins 从代码管理库中拉取代码时默认检出到工作空间的根目录,如果想修改的话可以用此参数。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     options {
    
     checkoutToSubdirectory('subdir')
    
     }
    
     stages {
    
     stage('test') {
    
         steps {
    
             // 步骤
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

4. disableConcurrentBuilds

作用:阻止 pipeline 同时执行。默认的情况下 pipeline 是可以同时执行多次的,如果为了防止同时访问共享资源或者防止一个较快的并发执行把较慢的并发执行给碾压的场景可以使用此参数禁止并发执行。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     options {
    
     disableConcurrentBuilds()
    
     }
    
     stages {
    
     stage('test') {
    
         steps {
    
             // 步骤
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

5. timeout

作用:为流水线的执行设置一个超时时间。如果超过这个值就会把整个流水线终止。时间单位可以是 SECONDS(秒),MINUTES(分钟),HOURS(小时)。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     options {
    
     timeout(time:10, unit:'HOURS')
    
     }
    
     stages {
    
     stage('test') {
    
         steps {
    
             // 步骤
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

6. skipDefaultCheckout

作用:删除隐式的 checkout scm 语句,因此可以从 Jenkinsfile 定义的流水线中跳过自动的源码检出功能。

代码示例:

复制代码
 options {

    
     skipDefaultCheckout()
    
 }
    
    
    
    
    AI助手

6、parameters

parameters 用来在 pipeline 中实现参数化构建,也就是根据用户指定的不同参数执行不同的操作。pipeline 支持很多种类型的参数,有字符串参数,布尔选择参数,下拉多选参数等。

1. 字符串参数

作用:开始构建前需要用户输入字符串,比如 ip 或者 url。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     parameters {
    
     string(name: 'DEPLOY_ENV', defaultValue: 'release', description: '')
    
     }
    
 }
    
    
    
    
    AI助手

2. 布尔值参数

作用:定义一个布尔类型参数,在执行构建前用户在 Jenkins UI 上选择是还是否,选择是执行这部分代码,否则会跳过这部分。比如:执行完毕后环境的清理工作。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     parameters {
    
     booleanParam(name: 'DEBUG_BUILD', defaultValue: true, description: '')
    
     }
    
 }
    
    
    
    
    AI助手

3. 文本参数

作用:支持写很多行的字符串。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     parameters {
    
     text(name: 'Welcome_text', defaultValue: 'One\nTwo\nThree\n',description: '')
    
     }
    
 }
    
    
    
    
    AI助手

4. 选择参数

作用:支持用户从多个选择项中选择一个值用来表示这个变量的值。比如:选择服务器类型、选择版本号等。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     parameters {
    
     choice(name: 'ENV_TYPE', choices: ['test', 'dev', 'product'], description: 'testmeans test env,….')
    
     }
    
 }
    
    
    
    
    AI助手

5. 文件参数

作用:参数化构建 UI 上提供一个文件路径的输入框,Jenkins 会自动去根据用户提供的网络路径去查找并下载。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     parameters {
    
     name: 'FILE', description: 'Some file to upload')
    
     }
    
 }
    
    
    
    
    AI助手

6. 密码参数

作用:密码(password)参数就是在 Jenkins 参数化构建 UI 提供一个暗文密码输入框。

例如,需要登录到服务器做自动化操作,为了安全起见,就不能用名为的 string 类型参数,而是 password 方式。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     parameters {
    
     password(name: 'PASSWORD', defaultValue: 'test', description: 'A secret password')
    
     }
    
 }
    
    
    
    
    AI助手

测试执行:

注意:

保存之后,左侧菜仍然单是 Build now,而并不是 Build with Parameters,这个是正常的,需要先点击 Build now,先完成第一个构建,Jenkins 第二个构建才会显示代码中的三个参数。刷新之后,就可以看到参数化构建这个菜单。

其它参数可以通过安装插件实现,比如Extended Choice Parameter(复选框插件)。

复制代码
 extendedChoice(description: '服务器(仅在部署、启动、关闭、状态操作时需要选择)', value: '192.168.1.100,192.168.1.101',

    
         descriptionPropertyValue: '192.168.1.100(测试),192.168.1.101(正式)',
    
         multiSelectDelimiter: ',', name: 'servers', quoteValue: false, saveJSONParameterToFile: false, type: 'PT_CHECKBOX', visibleItemCount: 5)
    
    
    
    
    AI助手

在阶段中调用上边设置的参数:

复制代码
 stage('编译打包') {

    
     steps {
    
     script {
    
         if (params.is_build == true) {
    
             build()
    
         }
    
         else{
    
             echo "=====跳过编译打包"
    
         }
    
     }
    
     }
    
 }
    
 stage('部署') {
    
     steps {
    
     script {
    
         if ("${params.select}".trim() == "部署") {
    
            for(ip in servers.tokenize(",")){
    
                  deploy(ip)
    
            }
    
         }
    
         
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

7、tool

Global Tool Configuration(全局工具配置)中配置的工具, tools指令能帮助我们自动下载并安装所指定的构建工具,并将其加入 PATH 变量中。这样,我们就可以在sh步骤里直接使用了。

对于 agent none,这个关键字将被忽略,因为没有任何节点或者代理可以用来安装工具。

tools指令默认支持3种工具:JDK、Maven、Gradle。通过安装插件,tools 指令还可以支持更多的工具。

复制代码
 tools {

    
 git ‘Default’
    
 jdk ‘JAVA_HOME’
    
 maven ‘MAVEN_HOME’
    
 }
    
    
    
    
    AI助手

一旦配置完成,tools 指令可以让我们指定的工具需要在我们已经选择的代理节点上自动安装在配置路径下。

代码示例 1:

复制代码
 pipeline {

    
     agent any
    
     tools {
    
     jdk 'jdk1.8'
    
     }
    
     stages {
    
     stage('test') {
    
         steps {
    
             sh 'java -version'
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

说明:

复制代码
 tools {

    
     jdk 'jdk1.8'
    
 }
    
    
    
    
    AI助手

左侧的 jdk 是流水线模型中定义的特殊字符串,目前,在声明式流水线中可以指定的合法的工具类型如下:ant、git、gradle、jdk、maven、jgit 等。

右侧的 jdk8 映射的是全局工具配置中的名称字段(管理 Jenkins → Global ToolConfiguration 中预配置)。例如,上面代码我写了 jdk1.8,那么必须在 Jenkins 管理-->全局工具配置中有别名为 jdk1.8 配置。

一旦这样设置后,jdk 工具会被自动安装到指定的路径下,然后我们可以在流水线步骤中简单的使用 jdk1.8 字符串替代 JAVA_HOME 路径,Jenkins 会把它映射到我们系统中安装的 JDK。、

代码示例 2:

复制代码
 pipeline{

    
     agent any
    
     parameters {
    
     string(name: 'gradleTool', defaultValue: 'gradle3', description: 'gradle version')
    
     }
    
     tools{
    
     gradle "${params.gradleTool}"
    
     }
    
 }
    
    
    
    
    AI助手

说明:如果需要输入一个特定的版本来使用,这个 tools 指令可以使用一个参数的值。

注意:当前生命式语法有一个局限就是当这个流水线第一次运行时,Jenkins 不能识别出该构建需要一个参数,需要先手动构建一次。

8、when

when{…} 是写在 stage{…} 里面一层条件控制,允许 pipeline 根据给定的条件来决定是否执行某个阶段。when 指令必须至少包含一个条件,也可以含多个条件,这与子条件嵌套在一个 allOf 条件中一样。

更复杂的条件结构可使用嵌套条件建:not,allOf 或 anyOf,嵌套条件可以嵌套到任意深度。下面来看看 when{…} 支持的一些内置条件命令。

1. branch

作用:当正在构建的分支与给出的分支模式匹配时执行。注意,仅适用于多分支 pipeline。

代码示例:

复制代码
    when { branch 'master' }
    
    AI助手

2. environment

作用:当指定的环境变量与给定的值相同时执行。

代码示例:

复制代码
    when { environment name: 'DEPLOY_TO', value: 'production' }
    
    AI助手

3. expression

作用:当给定的 Groovy 表达式返回 true 时执行。

代码示例:

复制代码
    when { expression { return params.DEBUG_BUILD } }
    
    AI助手

4. not

作用:当嵌套条件为 false 时执行,必须包含一个条件。

代码示例:

复制代码
    when { not { branch 'master' } }
    
    AI助手

5. allOf

作用:当所有嵌套条件都为真时执行,必须至少包含一个条件。

代码示例:

复制代码
 when {

    
     allOf {
    
     branch 'master';
    
     environment name: 'DEPLOY_TO',
    
     value: 'production' 
    
     }
    
 }
    
    
    
    
    AI助手

6. anyOf

作用:当至少一个嵌套条件为真时执行,必须至少包含一个条件。

代码示例:

复制代码
    when { anyOf { branch 'master'; branch 'staging' } }
    
    AI助手

7. buildingTag

作用:如果 pipeline 所执行的暧昧被打了 tag 则执行。

代码示例:

复制代码
    when { buildingTag() }
    
    
    AI助手

测试代码:

复制代码
 pipeline {

    
     agent any
    
     environment {
    
     quick_test = false
    
     }
    
     stages {
    
     stage('Example Build') {
    
         steps {
    
             script {
    
                 echo 'Hello World'
    
             }
    
         }
    
     }
    
     stage('Example Deploy') {
    
         when {
    
             expression {
    
                 return (quick_test == "true" )
    
             }
    
         }
    
         steps {
    
             echo 'Deploying'
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

environment 里面定义了一个键值对“quick_test = false”, 第二个 stage('ExampleDeploy') 因为不满足 when{…}里面的条件就不会执行。

9、input

input用户在执行各个阶段的时候,由人工确认是否继续进行。

  • message呈现给用户的提示信息。
  • id可选,默认为stage名称。
  • ok默认表单上的ok文本。
  • submitter可选的,以逗号分隔的用户列表或允许提交的外部组名。默认允许任何用户·
  • submitterParameter环境变量的可选名称。如果存在,用submitter名称设置。
  • parameters提示提交者提供的一个可选的参数列表。

input支持内部使用普通参数(单行文本、布尔、选项参数等):

复制代码
 pipeline {

    
     agent any
    
     stages {
    
     stage('Example') {
    
         input {
    
             message "Should we continue?"
    
             ok "Yes, we should."
    
             submitter "alice,bob"
    
             parameters {
    
                 extendedChoice(name: 'servers', description: '服务器', value: '192.168.1.100,192.168.1.101',
    
                             descriptionPropertyValue: '192.168.1.100(测试),192.168.1.101(正式)',
    
                             multiSelectDelimiter: ',', quoteValue: false, saveJSONParameterToFile: false, type: 'PT_CHECKBOX', visibleItemCount: 5)
    
                 string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
    
             }
    
         }
    
         steps {
    
             echo "Hello, ${PERSON}, nice to meet you."
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

10、script

作用:用来在声明式流水线中写非声明式代码的地方,允许你定义一定代码块/闭包用来囊括非声明式的语法,其实就是在 script 中写 Groovy 代码。比如 if...else 等语句。

代码示例:

复制代码
 pipeline {agent any

    
     stages {
    
     stage('test') {
    
     steps {
    
         script{
    
             def browsers=['chrome','firefox']
    
             for(int i=0;i<browsers.size();i++){
    
                 echo "testing the ${browsers[i]} browser"
    
                 }
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

11、triggers

用来指定使用什么类型的触发器来自动启动流水线构建。注意:这些触发器并不适合用于多分支流水线、github 组织等类型的任务。

有 4 种不同的触发器:cron、pollSCM、upstream 以 githubPush。

1. cron

作用:按照指定的周期有规律的执行流水线,就是闹钟一样,一到时间点就执行。

定时执行就像cronjob一样,一到时间点就执行。Jenkins采用了UNIX任务调度工具CRON的配置方式,用5个字段代表5个不同的时间单位(中间用空格隔开),语法如下:

字段 * * * * *
含义 分钟 小时 日期 月份 星期
取值范围 0-59 0-23 1-31 1-12 0-7

代码示例:

复制代码
 pipeline {

    
     agent any
    
     triggers {
    
     cron ('0 0 * * *')
    
     } 
    
     stages {
    
     stage('test') {
    
         steps {
    
             // 步骤
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

说明:

cron 包含 5 个字段,这些字段以空格或者 Tab 键分割,用来指定多久去执行一次构建。格式为:

  • MINUTES:一小时内的分钟,取值范围(0-59)
  • HOURS:一天内的小时,取值范围(0-23)
  • DAYMONTH :一个月中的某一天,取值范围(1-31)
  • MONTH :月份,取值范围(1-12)
  • DAYWEEK:一周中的星期几,取值范围(0-7)。0 和 7 都表示星期日

还可以使用特殊的字符一次指定多个值:

  • *:匹配所有的值
  • N:匹配 M-N 之间的值
  • M-N/或者*/:表示每隔,比如*/5 每隔 5 分钟
  • A,B,...Z:多个枚举值
  • H:可以用于任何字段,用来告诉 Jenkins 在一个范围内使用该项目名的散列值计算出一个唯一的偏移量,这个偏移量于范围内的最小值相加后定义为实际的执行时间。注意:这个值是项目名的散列值,那么每一个值都与其他项目是不同的,但是同一个项目的值是不变的。

H 符号在实际的项目中是非常推荐使用的,因为在大型的项目中可能存在多个同一时刻需要执行任务,比如(0 0 * * *),都需要在半夜零点启动,那么使用 H 后,从散列算法获得的偏移量,就可以错开执行具有相同 cron 时间的项目。

2. 事件触发

其它事件触发了pipeline执行,这个事件可以是在界面上手动触发、其它job触发、Http API Webhook触发等。

1)由上游任务触发:upstream

作用:由上游的任务构建完毕后触发本任务的执行。比如说需要先执行完编译打包和发布后才能执行测试脚本。

代码示例:

复制代码
 triggers {

    
     upstream threshold: 'UNSTABLE', upstreamProjects: 'jobA,jobB'
    
 }
    
    
    
    
    AI助手

说明:

upstreamProjects:指定上游任务的名称,有多个任务时用逗号分隔 。

threshold:指定上游任务的执行结果是什么值时触发,是枚举类型 hudson.model.Result 的某一个值,包括:

  • SUCCESS:构建成功;
  • UNSTABLE:存在一些错误,但不至于构建失败;
  • FAILURE:构建失败。
复制代码
 pipeline{

    
     agent any
    
     //说明:当test_1或者test_2运行成功的时候,自动触发
    
     triggers { upstream(upstreamProjects: 'test_1,test_2', threshold: hudson.model.Result.SUCCESS) }
    
     stages{
    
     stage("stage1"){
    
         steps{
    
             echo "hello"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

hudson.model.Result是一个枚举,包括以下值:

  • ABORTED 任务被手动中止;
  • FAILURE 构建失败;
  • SUCCESS 构建成功;
  • UNSTABLE 存在一些错误,但不至于构建失败;
  • NOT_BUILT 在多阶段构建时,前面阶段的问题导致后面阶段无法执行;

注意:需要手动触发一次 pipeline 的执行,让 Jenkins 加载 pipeline 后,trigger 指令才会生效。

2)Gitlab事件触发

当gitlab发现源代码有变化时,触发jenkins执行构建。

需要安装插件:

在pipeline中实现Gitlab trigger:

复制代码
 pipeline {

    
     agent any
    
     triggers {
    
     gitlab(triggerOnPush: true,
    
     triggerOnMergeRequest: true,
    
     branchFilterType: "All",
    
     secretToken: "rvgtcxwufgbcsr56lftzr5a74vhjko0")
    
     }
    
     stages {
    
     stage('pull') {
    
         steps {
    
             echo '拉取代码'
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

进入GitLab项目的配置页,找到设置->集成选项,把Jenkins暴露webhook及secretToken填入相应选项。

3. pollSCM

作用:轮询代码仓库,也就是定期到代码仓库询问代码是否有变化,如果有变化就执行。语法和 cron 是一样的。理论上轮询的越频繁越好,但是在一定的构建时间内,如果有多次代码提交,当构建失败时无法马上知道是哪一次的提交导致的,所以这个指令不是很常用,一般是用 webhook,当有代码提交的时候主动通知到 Jenkins。

代码示例:

复制代码
 pipeline {

    
     agent any
    
     triggers {
    
     pollSCM('h/5 * * * *')
    
     }
    
     stages {
    
     stage('test') {
    
         steps {
    
             // 步骤
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

4. Generic Webhook Trigger

需要安装 Generic Webhook Trigger(简称 GWT)插件才能使用。安装完之后,Jenkins 会暴露一个 API:http://<Jenkins_URL>/generic-webhook-trigger/invoke,当 GWT 插件接收到 JSON 或者 XML 的 HTTP POST 请求后,根据请求的参数和 Jenkins 中项目的匹配情况来决定执行哪个 Jenkins 项目。注意:项目创建完成后,一定要手动执行一次,这样 pipeline 的触发条件才会生效。

1)Generic Webhook Trigger 触发条件的结构

代码格式:

复制代码
 triggers {

    
     GenericTrigger
    
     causeString: 'Generic Cause',
    
     genericHeaderVariables: [[key: 'header', regexpFilter: '']],
    
     genericRequestVariables: [[key: 'request', regexpFilter: '']],
    
     genericVariables: [[defaultValue: '', expressionType: 'XPath', key: 'ref', regexpFilter:'', value: '$.ref']],
    
     printContributedVariables: true,
    
     printPostContent: true,
    
     regexpFilterExpression: '',
    
     regexpFilterText: '',
    
     token: 'secret'
    
 }
    
    
    
    
    AI助手

总结起来可以分为 5 部分:

  • genericVariables/genericHeaderVariables/genericRequestVariables:从 HTTP POST 请求提取参数。
  • token:用于标识待触发的 Jenkins 项目。
  • regexpFilterExpression/regexpFilterText:根据请求参数判断是否触发 Jenkins 项目的执行。
  • printContributedVariables/printPostContent/causeString:日志打印控制。
  • webhook:响应控制。

2)提取请求参数

genericVariables:提取 POST body 中的请求参数。

用法:

复制代码
    genericVariables: [[defaultValue: '', expressionType: 'XPath', key: 'ref', regexpFilter: '',value: '$.ref']]
    
    
    AI助手

说明:

  • expressionType: 可选,'XPath'默认是 JSONPath,采用默认值的话不会有此参数,还可以设置为 XPath。
  • value:JSON 表达式或者 XPath 表达式,取决于 expressionType 的类型。
  • key:一个变量名,用来存储从 POST body 提取出的值,可以用于 pipeline 其它步骤。
  • defaultValue:可选,当没有提取到时使用此值返回。
  • regexpFilter:可选,过滤表达式,用来对提取出的值进行过滤。

genericHeaderVariables:从 URL 中提取参数。

用法: genericHeaderVariables: [[key: 'header', regexpFilter: '']]

说明:

  • key:一个变量名,用来存储从 URL 提取出的值,可以用于 pipeline 其它步骤。
  • regexpFilter:对提取出的值进行过滤。
  • genericRequestVariables:从 HTTP Header 中提取参数。和 genericHeaderVariables 用法类似。

3)token

用法: token: 'secret'

说 明 : 用 来 标 识 一 个 pipeline 在 Jenkins 中 的 唯 一 性 。 当 Jenkins 接 收 到generic-webhook-trigger/invoke 接口的请求时,会将请求传递给 GWT 插件,GWT 插

件内部会遍历 Jenkins 中所有的项目,找到 Generic Webhook Trigger 配置 token 和请求中相同 token 的项目,触发这些项目的执行。

4)触发请求参数

如果配置了以下 2 项,即使 token 值匹配了,还要继续判断以下条件是否满足,才能真正触发项目的执行。

  • regexpFilterExpression:正则表达式。
  • regexpFilterText:需要匹配的 key。

5)日志打印控制

  • printContributedVariables:布尔类型,打印提取后的变量和变量值。
  • printPostContent:布尔类型,打印 webhook 请求信息。
  • causeString:字符串类型,用来表示 pipeline 被触发执行的原因,一般引用直接提取的变量。

12、parallel

声明式流水线的阶段可以在他们内部声明多隔嵌套阶段,它们将并行执行。注意,一个阶段必须只有一个steps或parallel的阶段。嵌套阶段本身不能包含进一步的parallel 阶段,但是其他的阶段的行为与任何其他stage parallel的阶段不能包含 agent 或 tools 阶段,因为他们没有相关 steps。

另外,通过添加failFasttrue到包含parallel的stage中,当其中一个进程失败时,你可以强制所有的parallel阶段都被终止。

复制代码
 pipeline {

    
     agent any
    
     stages {
    
     stage('Non-Parallel Stage') {
    
         steps {
    
             echo 'This stage will be executed first.'
    
         }
    
     }
    
     stage('Parallel Stage') {
    
         when {
    
             branch 'master'
    
         }
    
         failFast true
    
         parallel {
    
             stage('Branch A') {
    
                 agent {
    
                     label "for-branch-a"
    
                 }
    
                 steps {
    
                     echo "On Branch A"
    
                 }
    
             }
    
             stage('Branch B') {
    
                 agent {
    
                     label "for-branch-b"
    
                 }
    
                 steps {
    
                     echo "On Branch B"
    
                 }
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

13、借助 Jenkins 生成 pipeline 代码

我们对 pipeline 常用的语法有了初步的印象,但是如果完全记住所有的指令是不太现实的事情。其实 Jenkins 为我们提供了流水线语法在线生成的功能,我们基于表单的格式填好后,会 Jenkins 会帮助我们生成 pipeline 的代码。

进入任意一个 pipeline 项目,单击配置选项:

进入流水线选项卡,单击左下角的流水线语法:

选择“片段生成器”,可以查看所有内置的和已安装插件支持的命令:

以 dir 为例:

checkout 为例:


pipeline代码:

复制代码
 pipeline {

    
   agent any
    
   stages {
    
     stage('初始化') {
    
     steps {
    
         echo '初始化。。。'
    
     }
    
     }
    
     stage('打包') {
    
     steps {
    
         echo 'git代码拉取。。。'
    
         checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [], userRemoteConfigs: [[credentialsId: 'a8db6793-cc2b-4d82-bd3d-c5beb1c5149e', url: 'http://192.168.3.11/root/rapid-screen.git']]])
    
         
    
         echo 'mvn打包。。。'
    
         sh "/usr/local/apache-maven-3.8.2/bin/mvn -U clean install"
    
     }
    
     }
    
     stage('测试') {
    
     steps {
    
         echo '测试。。。'
    
     }
    
     }
    
     stage('发布') {
    
     steps {
    
         echo '发布。。。'
    
     }
    
     }
    
   }
    
 }
    
    
    
    
    AI助手


选择“Declarative Pipeline directive”,可以查看声明式 pipeline 的语法:

ipeline{} 声明式流水线的定义顶层。

agent{} 流水线运行节点:

  • any:任意节点;
  • label:根据节点标签选择;
  • none:当pipeline全局指定agent为none,则根据每个stage中定义的agent运行(stage必须指定);
  • node: 和label类似,可以添加些其他配置;

stages{} 定义pipeline阶段。

关系:stages>stage>steps>script。

复制代码
 pipeline {

    
     agent any
    
     stages {
    
     stage('build') {
    
         steps {
    
             echo 'Hello World'
    
             script {
    
                 sh 'echo "nihao"'
    
         }
    
         }
    
     }
    
     stage('test'){
    
         steps{
    
             echo "test"
    
         }
    
     }
    
     stage("deploy"){
    
         steps{
    
             echo "deploy"
    
         }
    
     }
    
  
    
     }
    
  
    
 }
    
    
    
    
    AI助手

post{} 根据流水线最终状态做相应的操作。

常见:

  • always: 不管什么状态总是执行;
  • success: 仅流水线成功后执行;
  • failure: 仅流水线失败后执行;
  • aborted: 仅流水线被取消后执行;
  • unstable:不稳定状态,单侧失败;

案例:

复制代码
 pipeline {

    
     // 选择运行节点
    
     //agent none
    
  
    
     agent {
    
     label 'master'
    
     }
    
  
    
     stages {
    
     stage('build') {
    
         // stage level agent
    
         agent { 
    
             label 'linux'
    
         }
    
         steps {
    
             echo 'build'
    
         }
    
     }
    
  
    
     stage('test') {
    
         steps{
    
             echo "test"
    
         }
    
     }
    
  
    
     stage('deploy'){
    
         steps{
    
             script{
    
                 // environment self
    
                 println("job name: ${JOB_NAME}")
    
             }
    
             echo "deploy"
    
         }
    
     }
    
  
    
     }
    
  
    
     post {
    
     always{
    
       echo "always"
    
       }
    
     success {
    
       echo "all stage success"
    
       }
    
     failure {
    
       echo "stage failure"
    
       }
    
     }
    
  
    
 }
    
    
    
    
    AI助手

environment{} 环境变量:

  • 流水线级别:pipeline{environment{}}。
  • 阶段级别:stage{environment{}}。

options{} 运行时的一些选项,以代码方式定义配置(需要构建一次,才能看到效果):

  • options { buildDiscarder(logRotator(numToKeepStr: '1')) } //设置保存最近的记录;
  • options { disableConcurrentBuilds() } //禁止并行构建;
  • options { skipDefaultCheckout() } //跳过默认的代码检出;
  • options { timeout(time: 1, unit: 'HOURS') } //设定流水线的超时时间(可用于阶段级别);
  • options { retry(3) } //设定流水线的重试次数(可用于阶段级别);
  • options { timestamps() } //设置日志时间输出(可用于阶段级别);

注:也可以在流水线创建界面选择操作。

案例:

复制代码
 pipeline {

    
     agent {
    
       label 'master'
    
     }
    
  
    
  
    
     //运行选项,pipeline级别
    
     options {
    
       disableConcurrentBuilds()  // 禁止并发构建
    
       buildDiscarder logRotator(artifactDaysToKeepStr: '', 
    
                             artifactNumToKeepStr: '', 
    
                             daysToKeepStr: '5', 
    
                             numToKeepStr: '8')  //历史构建
    
     }
    
  
    
     stages {
    
     stage('build') {
    
         options {  //stage级别
    
             timeout(time: 5, unit: 'MINUTES')
    
             retry(3)
    
         }
    
         steps {
    
             echo 'build'
    
         }
    
     }
    
     stage('deploy'){
    
         steps{
    
             script{
    
                 echo "deploy"
    
             }
    
  
    
         }
    
     }
    
  
    
     }
    
  
    
 }
    
    
    
    
    AI助手

parameters{} 代码的方式定义构建参数(需要构建一次,才能看到效果):

流水线在运行时设置的参数,UI页面的参数。所有的参数都存储在params对象中。

复制代码
 pipeline {

    
     agent {
    
       label 'master'
    
     }
    
     // 全局变量
    
     environment {
    
       VERSION = "1.0.0"
    
     }
    
     // 构建参数
    
     parameters {
    
       string defaultValue: 'ysh', description: 'this is name info', name: 'NAME'
    
       choice choices: ['dev', 'test', 'uat'], description: 'env names', name: 'ENVNAME'
    
     }
    
  
    
  
    
     stages {
    
     stage('build') {
    
         // stage level agent
    
         agent { 
    
            label 'linux'
    
         }
    
  
    
         // stage level env local
    
         environment {
    
           VERSION = "1.100.100"
    
         }
    
         steps {
    
             echo 'build'
    
  
    
             // 全局和局部变量
    
             echo "${VERSION}"
    
  
    
             // 打印构建参数值
    
             echo "姓名:${params.NAME}"
    
             echo "部署环境: ${params.ENVNAME}"
    
         }
    
     }
    
  
    
     }
    
  
    
 }
    
    
    
    
    AI助手

构建一次后,就可以选择了。

trigger 触发器:

cron 定时触发 : triggers { cron 'H/15 * * * *' }。

input{} :

  • message: 提示信息;
  • ok: 表单中确认按钮的文本;
  • submitter: 提交人,默认所有人可以;
  • parameters: 交互时用户选择的参数;

when{}:

常用:根据环境变量判断、表达式判断、条件判断(not/allOf/anyOf)。

paraller{} 并行发布:

复制代码
 pipeline {

    
     agent {
    
       label 'master'
    
     }
    
  
    
     // 构建触发器
    
     triggers {
    
       cron 'H * * * * '
    
     }
    
  
    
     stages {
    
     stage('build') {
    
         // stage level agent
    
         agent { 
    
            label 'linux'
    
         }
    
  
    
         
    
         steps {
    
             echo 'build'
    
  
    
         }
    
     }
    
  
    
     stage('test') {
    
         input {
    
           message '请确认'
    
           ok 'ok'
    
           submitterParameter 'approve_user'
    
           parameters {
    
             choice choices: ['deploy', 'rollback'], name: 'ops'
    
           }
    
         }
    
  
    
         steps{
    
             echo "test"
    
             echo "执行的动作: ${ops}"
    
             echo "批准用户: ${approve_user}"
    
             script {
    
                 // 由于下个stage无法获取ops的值,所以特此定义一个新的全局变量
    
                 // env. 定义全局变量
    
                 env.OPS = "${ops}"
    
             }
    
         }
    
     }
    
  
    
     stage('deploy'){
    
         // 是否运行
    
         when {
    
           environment name: 'OPS', value: 'deploy'
    
         }
    
  
    
         steps{
    
             echo "deploy"
    
         }
    
     }
    
  
    
     stage("parallelstage"){
    
         failFast true
    
         parallel {
    
             stage("build01"){
    
                 steps {
    
                     echo "windows"
    
                 }
    
             }
    
  
    
             stage("build02"){
    
                 steps{
    
                     echo "linux"
    
                 }
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手


四、Jenkins 共享库

当 Jenkins 上建立很多个 pipeline 项目的时候,就会有一些公共的代码在每个项目中都要去重复的编写,当这些代码需要做一些扩展或者修改的时候就需要把所有项目中全部改一遍,维护的成本太高,编写的效率太低。

比如发送邮件的功能是所有项目都需要的,邮件模板都写在每个 pipeline 项目中的话视觉效果极差。可以使用 pipeline 共享库(shared library)的技术解决这一问题。

1、共享库的代码结构

共享库有 3 个部分组成:resources 目录、src 目录和 vars 目录。

示例如下:

结构说明:

  1. resources 目录:一般将非 groovy 文件存放在此目录中,比如 XML 文件或者 JSON文件,可以通过外部库中的 libraryResource 步骤加载。
  2. src 目录:是使用标准 java 目录结构的 groovy 文件,目录中的类称为库类(Library class),这些类必须实现 Serializable 接口,以此保证在流水线停止或者重新启动时能正确的恢复。在流水线中使用 src 目录中的类时,需要注意要使用包名,同时因为是 groovy 代码,所以还要用 script 命令包起来。
  3. vars 目录:这个是我们要介绍的重点。此目录下存放的可以在 pipeline 中直接调用的全局变量,在 pipeline 中使用的函数名就是文件名,当文件中定义了 call 方法时,它可以像常规流水线步骤一样被调用。这个call方法就相当于这个公共方法的main方法。记住一个原则,一个公共方法,写一个 groovy 类文件,这个 groovy 文件的名称就是这个公共方法的函数名称,在其他 pipeline 项目中调用这个方法就是直接写这个groovy 类名称。

共享库的定义和使用一般分为以下 4 步:

  1. 根据工作实际需要编写共享库的源代码;
  2. 将源代码放到代码管理仓库中;
  3. 在 Jenkins 全局配置中定义共享库;
  4. 在 pipeline 项目或中 Jenkinsfile 中通过@Library 引用共享库。

2、编写共享库文件

注意:库文件中包含中文的话一定要保存为 ANSI 格式,否则可能会出现乱码的情况。

我们主要将常用的一些功能封装到 vars 目录来演示,其它高级的用法可以根据实际的需要进一步研究。

在 vars 目录下创建 2 个文件,分别名为 command.groovy 的文件,如下:

再创建名为 say.groovy 的文件,内容如下:

3、Jenkins 中配置共享库



4、通过@Library 注解使用共享库

在 pipeline 的上方使用@Library 引入共享库。

语法:@Library('[@]')_ []

说明:

  • libname:表示库名,必须要有。
  • version:版本号以@开头,可以是代码仓库的标签、分支名或者其他规范。
  • _:下划线,表示一次性静态加载 src 目录下所有的代码到 classpath 中,如果没有后面的 import 语句,就必须有此下划线。
  • import:可以没有,表示导入指定的方法,如果没有指定表示导入共享库中所有的方法。

使用示例:

复制代码
 //加载一个库的默认版本

    
 @Library('share-lib')_
    
  
    
 //加载一个库的指定版本
    
 @Library('share-lib@2.0')_
    
  
    
 //加载多个库
    
 @Library(['share-lib@release1.0','myLib'])_
    
  
    
 //带导入语句
    
 @Library('share-lib@2.0') import static org.demo.Utils.*
    
    
    
    
    AI助手

5、共享库格式化日志输出

安装AnsiColor插件:

点击流水线语法:

选择xterm方式;

tools.groovy:

复制代码
 package org.devops

    
  
    
 //格式化输出
    
 def PrintMes(value,color){
    
     colors = ['red'   : "\033[40;31m >>>>>>>>>>>${value}<<<<<<<<<<< \033[0m",
    
               'blue'  : "\033[47;34m ${value} \033[0m",
    
               'green' : "[1;32m>>>>>>>>>>${value}>>>>>>>>>>[m",
    
               'green1' : "\033[40;32m >>>>>>>>>>>${value}<<<<<<<<<<< \033[0m" ]
    
     ansiColor('xterm') {
    
         println(colors[color])
    
     }
    
 }
    
    
    
    
    AI助手

Jenkinsfile:

复制代码
 #!groovy

    
  
    
 @Library('jenkinslib') _
    
  
    
 def tools = new org.devops.tools()
    
  
    
  
    
  
    
 String workspace = "/opt/jenkins/workspace"
    
  
    
 //Pipeline
    
 pipeline {
    
     agent { node {  label "master"   //指定运行节点的标签或者名称
    
                 customWorkspace "${workspace}"   //指定运行工作目录(可选)
    
         }
    
     }
    
  
    
     options {
    
     timestamps()  //日志会有时间
    
     skipDefaultCheckout()  //删除隐式checkout scm语句
    
     disableConcurrentBuilds() //禁止并行
    
     timeout(time: 1, unit: 'HOURS')  //流水线超时设置1h
    
     }
    
  
    
     stages {
    
     //下载代码
    
     stage("GetCode"){ //阶段名称
    
         when { environment name: 'test', value: 'abcd' }
    
         steps{  //步骤
    
             timeout(time:5, unit:"MINUTES"){   //步骤超时时间
    
                 script{ //填写运行代码
    
                     println('获取代码')
    
                     tools.PrintMes("获取代码",'green')
    
                     println("${test}")
    
                     
    
                     input id: 'Test', message: '我们是否要继续?', ok: '是,继续吧!', parameters: [choice(choices: ['a', 'b'], description: '', name: 'test1')], submitter: 'lizeyang,admin'
    
                 }
    
             }
    
         }
    
     }
    
  
    
     stage("01"){
    
         failFast true
    
         parallel {
    
     
    
             //构建
    
             stage("Build"){
    
                 steps{
    
                     timeout(time:20, unit:"MINUTES"){
    
                         script{
    
                             println('应用打包')
    
                             tools.PrintMes("应用打包",'green')
    
                             mvnHome = tool "m2"
    
                             println(mvnHome)
    
                             
    
                             sh "${mvnHome}/bin/mvn --version"
    
                         }
    
                     }
    
                 }
    
             }
    
     
    
             //代码扫描
    
             stage("CodeScan"){
    
                 steps{
    
                     timeout(time:30, unit:"MINUTES"){
    
                         script{
    
                             print("代码扫描")
    
                             tools.PrintMes("代码扫描",'green')
    
                         }
    
                     }
    
                 }
    
             }
    
         }
    
     }
    
     }
    
  
    
     //构建后操作
    
     post {
    
     always {
    
         script{
    
             println("always")
    
         }
    
     }
    
  
    
     success {
    
         script{
    
             currentBuild.description = "\n 构建成功!" 
    
         }
    
     }
    
  
    
     failure {
    
         script{
    
             currentBuild.description = "\n 构建失败!" 
    
         }
    
     }
    
  
    
     aborted {
    
         script{
    
             currentBuild.description = "\n 构建取消!" 
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

添加tools.PrintMes(),给输出的日志添加颜色。

我们业可以将Jenkins生成的代码封装到共享库中来引用,使得代码结构化,编写Jenkinsfile更加容易。

五、Pipeline 扩展插件

1、Pipeline Basic Steps 插件

pipeline basic steps 是 pipeline 最基础的一个插件,在安装 pipeline 的时候默认会自动安装,可以在 Jenkins 环境,插件管理下的installed下面找到这个插件。

代码生成器中常用的DSL方法:

  • catchError: 捕捉错误和设置构建结果为失败。
  • deleteDir: 在工作区内递归删除当前目录
  • dir: 设置当前工作目录。
  • echo: 打印信息
  • error: 设置构建结果和阶段结果为异常,并打印设置的信息,可以中断流水线的执行,也可以抛出新的Exception(),但是这个步骤不会打印堆栈信息。
  • fileExists: 检查文件是否存在,返回true|false。
  • isUnix: 检查是否运行在类unix节点上
  • mail: 邮件通知
  • pwd: 以字符串形式返回当前目录路径。
  • readFile: 从相对路径读取文件(通常是工作空间),并以普通字符串的形式返回其内容。
  • retry: 重试内部代码N次。
  • sleep: 暂停管道构建,直到给定的时间过期。
  • stash: 在构建的阶段中把文件保存起来,这个文件可以给当前构建中的其它阶段使用。
  • step: 一般的构建步骤
  • timeout: 设置执行超时时间,如果块内的代码执行时间超出限制,会抛出异常。
  • tool: 使用预定义的工具安装中的工具,参考2.2。
  • unstable: 设置构建结果和阶段结果设置为不稳定。并打印日志信息。
  • unstash: 取出之前stash保存的文件。
  • waitUntil: 反复运行它的内部代码块,直到返回true。如果返回false,等待一段时间并再次尝试。
  • warnError: 捕获错误并将构建和阶段结果设置为不稳定
  • withEnv: 设置环境变量
  • wrap: 一般构建包装
  • writeFile: 将给定的内容写入当前目录中。
  • archive: 把构建中输出的内容存档,供以后其它构建使用。Jenkins 2.x后,提供archiveArtifacts代替archive,该步骤已弃用。
  • getContext: G从内部api获取上下文对象。
  • unarchive: 将存档的工件复制到工作区中。
  • withContext: 在块内部使用上下文对象 API。

详情,可查看: Pipeline: Basic Steps

1. readJSON

处理JSON字符串常用方法。

复制代码
 def response = readJSON text: "${scanResult}"

    
 println(scanResult)
    
  
    
 //原生方法import groovy.json.*
    
  
    
 @NonCPS
    
 def GetJson(text) {
    
 def prettyJson = JsonOutput.prettyPrint(text)
    
 new JsonSlurperClassic().parseText(prettyJson)
    
 }
    
  
    
 def readJsonFile(filePath) {
    
     def propMap = readJSON file: filePath
    
     return propMap
    
 }
    
    
    
    
    AI助手

@NonCPS 处理频繁序列化操作。

2. readFile

作用:读取指定文件的内容,以字符串的形式返回。

参数说明:

  • file:相对于当前工作空间的文件路径,也可以用绝对路径表示;
  • encoding:读取文件时的编码方式,默认是根据你当前平台的编码去解析。如果读取的是二进制文件,会采用 Base64 转码的字符串输出。
复制代码
 import hudson.model.*;

    
  
    
 println env.JOB_NAME
    
 println env.BUILD_NUMBER
    
  
    
 pipeline{
    
     agent any
    
     stages{
    
     stage("init") {
    
         steps{
    
             script {
    
                 file = "../../jobs/test_pipeline_demo/demo.txt"
    
                 file_contents = readFile file
    
                 println file_contents
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

说明:将 jobs/test_pipeline_demo 目录下的 demo.txt 文件内容读出并打印。

3. writeFile

作用:writeFile 和 readFile 类似,是简单的文本文件进行写操作。如果明确知道是 json 或其他类型文件,那么就可以用其他插件的 readJson 来读取。

参数说明:

  • file:相对于当前工作空间的文件路径,也可以用绝对路径表示,路径不存在会创建。
  • text:要写入的文件内容。
  • encoding:写入文件时的编码方式,默认使用的是当前平台的编码。

4. deleteDir() 方法

作用:默认递归删除 WORKSPACE 下的文件和文件夹,这个方法是没有参数,一般与 dir 一起使用。当执行完每一个 stage 里面的代码,需要在 post{...}里面写一些 clean up 操作,如果这个操作是清空 WORKSPACE 的话,就可以使用 deleteDir()。特别是生产环境,需要节约 Jenkins 服务器的磁盘空间,清空 WORKSPACE 是很有必要的操作。

用法:deleteDir()

5. mail

邮件功能在 Jenkins 中是非常有用的,当构建完成后无论成功还是失败都需要将构建结果通知到相关人员,邮件是最常见的选择。

我们可以借助片段生成器查看该插件支持的参数:

需要注意的是 Body MIME Type 这个选项默认就是 text/plain,可以指定为 text/html。

生成的DSL方法:

6. dir

作用:切换操作目录。

代码示例:

复制代码
 import hudson.model.*;

    
  
    
 println env.JOB_NAME
    
 println env.BUILD_NUMBER
    
  
    
 pipeline{
    
     agent any
    
     stages{
    
     stage("dir") {
    
         steps{
    
             println env.WORKSPACE
    
             dir(".."){
    
                 echo "pwd"
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

7. echo("message")和 error("error_message")

作用:echo和 groovy 中的 println 没有任何区别。一般来说使用 echo 就是打印 info debug级别的日志输出用,如果遇到错误,就可以使用 error(“error message”),如果出现执行到 error 方法,Jenkins job 会退出并显示失败效果。

8. fileExists

作用:判断一个文件是否存在,返回值是布尔类型,true 就表示文件存在,false 表示文件不存在。

9. isUnix()

作用:判断当前运行的 Jenkins node 环境是 linux 还是 windows,如果返回是 true 表示是 linux/mac 系统,如果返回是 false,表示当前 Jenkins job 运行在 windows 的系统上。

复制代码
 import hudson.model.*;

    
  
    
 println env.JOB_NAME
    
 println env.BUILD_NUMBER
    
  
    
 pipeline{
    
     agent any
    
     stages {
    
     stage("isUnix") {
    
         steps{
    
             script {
    
                 if(isUnix() == true) {
    
                     echo("this Jenkins job running on a linux-like system")
    
                 }else {
    
                     error("the Jenkins job running on a windows system")
    
                 }
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

10. pwd()

作用:返回当前所在的目录。由于 Jenkins 支持 windows 和 linux,但是 linux 是 pwd,windows 上是 dir。所以这个插件就干脆支持一个方法,统称为 pwd()。

11. withCredentials

Jenkins中的凭据。

复制代码
 steps{

    
     withCredentials([usernamePassword(credentialsId: 'user_for_openshift', passwordVariable: 'password', usernameVariable: 'username')]) {
    
     sh 'docker login -u $username -p $password registory.ctiwifi.cn:5000
    
     }
    
 }
    
    
    
    
    AI助手

使用创建的凭据:

在流水线中测试以变量方式使用凭据:

12. checkout

checkout用来下载代码,一般有两种下载代码的方式。

13. publishHTML

单元测试,生成HTML报告。

14. input

交互式输入方式。

15. BuildUser

获取构建的用户。

16. httpRequest

给接口传递参数,需要安装插件,才能在代码生成器中生成选项。

17. stash

stash步骤可以将一些文件保存起来,以便被同一次构建的其他步骤或阶段使用。如果整个pipeline的所有阶段在同一台机器上执行,则stash步骤是多余的。

所以,通常需要stash的文件都是要跨Jenkins node使用的

stash步骤会将文件存储在tar文件中,对于大文件的stash操作将会消耗Jenkins master的计算资源。Jenkins官方文档推荐,当文件大小为5∼100MB时,应该考虑使用其他替代方案。

stash步骤的参数列表如下:

  • name:字符串类型,保存文件的集合的唯一标识。
  • allowEmpty:布尔类型,允许stash内容为空。
  • excludes:字符串类型,将哪些文件排除。如果排除多个文件,则使用逗号分隔。留空代表不排除任何文件。
  • includes:字符串类型,stash哪些文件,留空代表当前文件夹下的所有文件。
  • useDefaultExcludes:布尔类型,如果为true,则代表使用Ant风格路径默认排除文件列表。

除了name参数,其他参数都是可选的。

excludes和includes使用的是Ant风格路径表达式。

18. unstash

取出之前stash的文件。

unstash步骤只有一个name参数,即stash时的唯一标识。通常stash与unstash步骤同时使用。

以下是完整示例:

复制代码
 pipeline {

    
     agent none 
    
     stages {
    
     stage('stash'){
    
         agent { label "master" }
    
         steps {
    
             writeFile file:"a.txt",text:"$BUILD_NUMBER"
    
             stash(name:"abc",includes:"a.txt")
    
         }
    
     }
    
     stage('unstash'){
    
         agent { label "node2" }
    
         steps {
    
             script{
    
                 unstash("abc")
    
                 def content = readFile("a.txt")
    
                 echo "${content}"
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

stash步骤在master节点上执行,而unstash步骤在node2节点上执行。

19. sh、bat、powershell

与命令相关的步骤其实是Pipeline:Nodes and Processes插件提供的步骤。由于它是Pipeline插件的一个组件,所以基本不需要单独安装。

sh步骤支持的参数有:

  • script:将要执行的shell脚本,通常在类UNIX系统上可以是多行脚本。
  • encoding:脚本执行后输出日志的编码,默认值为脚本运行所在系统的编码。
  • returnStatus:布尔类型,默认脚本返回的是状态码,如果是一个非零的状态码,则会引发pipeline执行失败。如果returnStatus参数为true,则不论状态码是什么,pipeline的执行都不会受影响。
  • returnStdout:布尔类型,如果为true,则任务的标准输出将作为步骤的返回值,而不是打印到构建日志中(如果有错误,则依然会打印到日志中)。除了script参数,其他参数都是可选的。

returnStatus与returnStdout参数一般不会同时使用,因为返回值只能有一个。如果同时使用,则只有returnStatus参数生效。

bat步骤执行的是Windows的批处理命令。powershell步骤执行的是PowerShell脚本,支持3+版本。这两个步骤支持的参数与sh步骤的一样。

20. 其他DSL方法

1)cleanWs

清理空间:

复制代码
 // 删除${WORKSPACE}目录

    
 cleanWs()
    
  
    
 // 判断目录是否存在
    
 dir("${env.WORKSPACE}@tmp") {
    
     //删除${WORKSPACE}@tmp目录
    
     deleteDir()
    
 }
    
    
    
    
    AI助手

2)error

主动报错,中止当前pipeline。

error 步骤的执行类似于抛出一个异常。它只有一个必需参数:message。通常省略参数:error("there's an error")。

3)tool

使用预定义的工具。

如果在Global Tool Configuration(全局工具配置)中配置了工具,比如配置了Docker,那么可以通过tool步骤得到工具路径。

tool步骤支持的参数有:

  • name:工具名称。
  • type(可选):工具类型,指该工具安装类的全路径类名。

每个插件的type值都不一样,而且绝大多数插件的文档根本不写type值。除了到该插件的源码中查找,还有一种方法可以让我们快速找到type值,就是前往Jenkins pipeline代码片段生成器中生成该tool步骤的代码即可

4)timeout

代码块超时时间。

为timeout步骤闭包内运行的代码设置超时时间限制。如果超时,将抛出一个org.jenkinsci.plugins.workflow.steps.FlowInterruptedException异常。

timeout步骤支持如下参数:

  • time:整型,超时时间。
  • unit(可选):时间单位,支持的值有NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS、MINUTES(默认)、HOURS、DAYS。
  • activity(可选):布尔类型,如果值为true,则只有当日志没有活动后,才真正算作超时。

5)waitUntil

等待条件满足。

不断重复waitUntil块内的代码,直到条件为true。waitUntil不负责处理块内代码的异常,遇到异常时直接向外抛出。waitUntil步骤最好与timeout步骤共同使用,避免死循环。

示例如下:

复制代码
 timeout(50){

    
     waitUntil {
    
     script {
    
         def r = sh script: 'curl http://exmaple', returnStatus:true 
    
         return (r == 0)
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

6)sleep

让pipeline休眠一段时间。

sleep步骤可用于简单地暂停pipeline,其支持的参数有:

  • • time:整型,休眠时间。
  • • unit(可选):时间单位,支持的值有NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS(默认)、MINUTES、HOURS、DAYS。

示例如下:

复制代码
 sleep(120)//休眠120秒

    
  
    
 sleep(time:'2', unit:"MINUTES") // 休眠2分钟
    
    
    
    
    AI助手

7)junit单元测试报告

在Jenkins中加入junit步骤。通常将junit步骤放在post always中,因为当测试不通过时,我们依然可以收集到测试报告。

写法如下:

复制代码
 post {

    
     always{
    
     junit testResults: "**/target/surefire-reports/*.xml"
    
     }
    
 }
    
    
    
    
    AI助手

当pipeline运行结束后,在构建页的左边菜单栏及右边详情下都会多出一个链接:Test Result。

单击“Test Result”进入,可以看到测试报告的详细信息。

junit步骤的testResults参数支持Ant风格路径表达式。**/target/surefire-reports/*.xml表示只要是target/surefire-reports目录下的XML文件就会被当作JUnit测试报告处理,而不论target在哪个层级的目录下。

8)JMeter性能测试

Taurus是一个开源的自动化框架,用于运行各种开源负载测试工具和功能测试工具。其支持最流行的开源负载测试工具Apache JMeter、Selenium、Gatling、The Grinder等。

Taurus的关键特性有:

  • 我们可以使用YAML或JSON来描述性能测试。这也正是我们想要的test as code。
  • 它会根据我们选择的性能测试类型自动下载相应的工具。比如在下例中会使用JMeter,那么Taurus会自动下载JMeter并安装。

Jenkins的Performance插件就是使用Taurus来进行性能测试的。在进行性能测试之前,首先要准备环境。

准备性能测试环境:

  1. 在运行性能测试环境的机器上,准备Python环境。
  2. 安装Performance插件(https://plugins.jenkins.io/performance)。
  3. 安装Taurus?不需要自行安装,Performance插件如果发现机器上没有安装Taurus,它会自动运行pip install bzt命令进行安装。

运行JMeter测试:

假设平时你都是手动执行JMeter测试的,现在希望将它自动化。这很简单,只需要两步。

(1)在现有的项目中加入Jenkinsfile。

复制代码
 pipeline {

    
     agent any
    
  
    
     stages {
    
     stage('performance test') {
    
         steps {
    
             bzt params: 'blaze_exist_jmeter_config.yml'
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

(2)在项目中加入blaze_exist_jmeter_config.yml文件。

复制代码
 execution:

    
     - scenario: simple
    
  
    
 scenarios:
    
     simple:
    
     script: SimpleTestPlan.jmx
    
  
    
 modules:
    
     jmeter:
    
     # 注意,下载文件必须使用.zip后缀
    
     download-link: http://mirrors.tuna.tsinghua.edu.cn/apache//jmeter/binaries/apache-jmeter-{version}.zip
    
     version: 5.0
    
    
    
    
    AI助手

blaze_exist_jmeter_config.yml是Taurus的配置文件,用于描述如何进行性能测试。以上配置很简单,就是执行一个名为simple的场景(scenario),这个场景就是执行现有的JMeter脚本。

modules配置了JMeter的下载地址及版本。

上例中,我们指定了国内的下载链接,避免从国外下载。在Jenkinsfile中,bzt是Performance插件提供的一个步骤。

其参数如下:

  • params:字符串类型,Taurus配置文件的路径。
  • alwaysUseVirtualenv:布尔类型,如果为false,则不使用virtualenv进行环境隔离。默认值为true。
  • bztVersion:字符串类型,bzt版本。
  • generatePerformanceTrend:布尔类型,是否在Jenkins项目详情页生成性能趋势图。默认值为true。
  • useBztExitCode:布尔类型,是否使用bzt步骤的退出码作为Jenkins项目的构建结果。默认值为true。
  • useSystemSitePackages:布尔类型,是否为virtualenv加上“--system-site-packages”参数。默认值为true。
  • workingDirectory:字符串类型,指定bzt的工作目录。
  • workspace:字符串类型,已经废弃,请使用workingDirectory。

只有params参数是必需的,其他参数都是可选的。

至此,以上用法可以满足大部分人在Jenkins上使用JMeter的需求。关于Taurus配置文件的更多语法,大家可以前往Taurus官网学习。

9)K8s

2、Job DSL 插件

1. Job DSL Plugin和 Pipeline Plugin 区别

Job DSL已经存在了很长时间,并且是Netflix的"编码"Jenkins编码的开源解决方案。

Job DSL 是 Jenkins 最早流行的插件之一,它允许将配置管理为代码,从那时起,许多其他处理这方面的插件已经创建,最著名的是Jenkins PipelineConfiguration as Code插件。了解这些插件和 Job DSL 之间的差异对于有效管理 Jenkins 配置很重要。

Job DSL 允许您在编写Jenkins作业的脚本中引入逻辑和变量,并且通常可以使用这些作业为特定项目形成某种"管道"。该插件作为启用工作模板和脚本编写的常用方法而受到了广泛的关注。

Jenkins Pipeline(2.0)是Jenkins作业的一种新形式,它完全基于DSL,并试图消除将多个作业缝合在一起以填充单个管道的需要,这是迄今为止Job DSL的最常用用法。

最初,由于Pipeline DSL不提供Job DSL所具有的许多功能,并且如上所述,Job DSL允许您创建Pipeline作业,因此可以将它们一起使用以定义管道。管道是新的本机Jenkins功能,可将作业作为代码。

但是,如果从头开始构建Jenkins,则仍需要创建这些作业。这意味着Jenkins不能100%真正地由代码编写和构建。您可以做的是使用JOB DSL构建所有作业的框架结构,然后使用管道执行作业。这将允许您100%编写Jenkins脚本,减去要创建的初始种子作业。

如今,几乎没有理由使用Job DSL,因为Pipeline是Jenkins支持的Jenkins管道脚本编写机制,并且已经达到或超越了Job DSL的许多功能。Jenkins 2.0专注于流水线作为代码,这意味着Job DSL没有未来,或Pipeline Plugin是Job DSL插件的下一步。

Pipeline管道具有以下优点:

  • 不需要像使用Job那样使用管道"播种"作业
    DSL是因为管道本身就是工作。有了Job DSL,
    只是创建其他作业的脚本。

  • 使用管道,您可以使用诸如参数化手动输入步骤之类的功能,从而可以在管道内指定逻辑中途

  • 可以是的逻辑
    作业DSL中包含的内容仅限于创建作业
    他们自己;而使用管道,您可以直接包含逻辑
    在工作中。

  • 使用Build Pipeline Plugin创建作业基本DSL的工作要困难得多。使用管道,您的文件将更小,语法更短。而且,如果您正在使用Job DSL创建Pipeline作业,那么Jenkins Pipeline即可提供模板功能,因此我没有看到它的主要价值。

也许最终,我们将能够使用管道来完全控制Jenkins(安全性,配置甚至插件),但是到那时之前,使用Job DSL仍是一种很好的方法。

最后,Jenkins Pipeline是迄今为止Jenkins最流行的功能。查看Jenkins World 2016议程,您将看到大约,50%的会话涉及管道,而对于Job DSL则无。

2. Job DSL 插件

Jenkins 是一个用于管理构建的出色系统,人们喜欢使用它的 UI 来配置作业。不幸的是,随着作业数量的增加,维护它们变得乏味,并且使用 UI 的范例也分崩离析,很难保持这些工作之间的一致性。

Job DSL 插件试图通过允许在人类可读文件中以编程形式定义作业来解决这个问题。无需成为 Jenkins 专家即可编写这样的文件,因为 Web UI 的配置可以直观地转换为代码。

详见官网:

GitHub - jenkinsci/job-dsl-plugin: A Groovy DSL for Jenkins Jobs - Sweeeeet!

安装插件:

在搜索框中,输入Job DSL。接下来,在生成的插件列表中,选中 Job DSL 旁边的框,然后单击 Install without restart。

注意:如果搜索Job DSL没有返回结果,则表示 Job DSL 插件已安装,或者您的 Jenkins 服务器的插件列表未更新。

您可以通过导航到your_jenkins_url/pluginManager/installed并搜索Job DSL来检查是否已安装 Job DSL 插件。

您可以通过导航到your_jenkins_url/pluginManager/available并单击(空)插件列表底部的 Check Now 按钮来更新 Jenkins 服务器的插件列表。

启动安装过程后,您将被重定向到显示安装进度的页面。等到您在 Job DSL 和 Loading plugin extensions 旁边看到 Success,然后再继续下一步。

Job DSL 允许将配置管理为代码:

上面的作业配置可以从以下代码生成:

复制代码
 pipelineJob('job-dsl-plugin') {

    
   definition {
    
     cpsScm {
    
       scm {
    
     git {
    
       remote {
    
         url('https://github.com/jenkinsci/job-dsl-plugin.git')
    
       }
    
       branch('*/master')
    
     }
    
       }
    
       lightweight()
    
     }
    
   }
    
 }
    
    
    
    
    AI助手

Pipeline 插件支持通过 Pipeline DSL 实现和集成持续交付管道。虽然可以使用 Job DSL 结合 Jenkins 生态系统中的许多插件使用自由式作业创建复杂的管道,创建和维护这些管道,包括为各个 SCM 分支生成作业以及可能并行运行步骤以提高构建性能,构成一个重大的挑战。

Jenkins Pipeline 通常是创建复杂自动化流程的更好选择。Job DSL 可用于创建流水线和多分支流水线作业。不要混淆 Job DSL 和 Pipeline DSL,两者都有自己的语法和适用范围。

配置即代码插件可用于管理 Jenkins 的全局系统配置,它集成了 Job DSL 以创建一组初始作业。

3. Job DSL 快速入门

已经安装了 Job DSL 插件,现在可以使用 Job DSL 将作业配置为代码。

这里将在 Job DSL 脚本中定义一个演示作业,然后将脚本合并到一个种子作业中,该作业在执行时将创建已定义的作业。

种子工作是一项创造更多工作的工作。在这一步中,您将构建一个 Job DSL 脚本并将其合并到一个种子作业中。您将定义的 Job DSL 脚本将创建一个自由式作业,该作业在作业的控制台输出中打印'Hello World!'消息。

Job DSL 脚本由 Job DSL 插件提供的 API 方法组成;您可以使用这些 API 方法来配置作业的不同方面,例如其类型(自由式作业与管道作业)、构建触发器、构建参数、构建后操作等。您可以在API 参考站点上找到所有支持的方法。

详见:Jenkins Job DSL Plugin

对于“Hello World”自由式作业,您需要job API 方法(freeStyleJob是job的别名,也可以使用)。

单击job(String name) { … }中的省略号图标 (…) 以显示job块中可用的方法和块。

job块中的一些最常用的方法和块:

  • parameters:设置用户在创建作业的新版本时输入的参数。
  • properties:要在作业中使用的静态值。
  • scm:配置如何从 GitHub 等源代码控制管理提供商检索源代码。
  • steps:构建每个步骤的定义。
  • triggers:除了手动创建构建之外,还指定作业应该在什么情况下运行(例如,定期像cron 作业,或者在一些事件之后,比如推送到 GitHub 存储库)。

您可以进一步展开子块以查看其中可用的方法和块。单击steps { … }中的省略号图标 (…) 以发现shell(String command)方法,您可以使用该方法运行 shell 脚本。

将这些部分放在一起,您可以编写如下所示的 Job DSL 脚本来创建一个自由式作业,该作业在运行时将在输出控制台中打印'Hello World!'。

复制代码
 job('demo') {

    
   steps {
    
     shell('echo Hello World!')
    
   }
    
 }
    
    
    
    
    AI助手

要运行 Job DSL 脚本,我们必须首先将其合并到种子作业中。

要创建种子作业,请转到your_jenkins_url,登录(如有必要),单击仪表板左侧的 New Item 链接。在随后的屏幕上,输入seed,选择 Freestyle project,然后单击 OK。

在随后的屏幕中,向下滚动到 Build 部分,然后单击 Add build step 下拉菜单。

添加“Process Job DSLs”构建步骤并将下面的脚本粘贴到“DSL Script”字段中。

然后,单击 Use the provided DSL script 旁边的单选按钮,并将您编写的 Job DSL 脚本粘贴到 DSL Script 文本区域。

在 Windows 上运行 Jenkins 时,shell一步一步替换batchFile。

复制代码
 job('demo') {

    
   steps {
    
     batchFile('echo Hello World!')
    
   }
    
 }
    
    
    
    
    AI助手

保存配置,回到seed作业页面,点击左侧的Build Now按钮,运行seed作业。

开始构建并检查控制台输出:

复制代码
 Started by user admin

    
 Running as SYSTEM
    
 Building in workspace /var/jenkins_home/workspace/seed
    
 Processing provided DSL script
    
 Added items:
    
     GeneratedJob{name='demo'}
    
 Finished: SUCCESS
    
    
    
    
    AI助手

刷新页面,你会看到一个新的部分,上面写着 Generated Items;它列出了您在 Job DSL 脚本中指定的demo作业。

种子作业已生成“示例”作业,在作业的配置页面上验证结果。

导航到your_server_ip,您将找到您在 Job DSL 脚本中指定的demo作业。

点击demo链接进入demo工作页面。您会看到 Seed job:seed,表示该作业是由seed作业创建的。现在,单击 Build Now 链接运行demo作业一次。

这会在 Build History 框中创建一个条目。将鼠标悬停在条目的日期上以显示一个小箭头;单击它以显示下拉菜单。从下拉列表中选择控制台输出。

这将为您带来此构建的日志和控制台输出。在其中,您将找到行+ echo Hello World!后跟Hello World!,它对应于 Job DSL 脚本中的shell('echo Hello World!')步骤。

您已运行demo作业并确认已执行 Job DSL 脚本中指定的echo步骤。在下一步也是最后一步中,您将修改并重新应用 Job DSL 脚本以包含一个额外的管道作业。

根据 Everything as Code 范例,越来越多的开发人员选择将其构建定义为管道作业——那些使用管道脚本(通常命名为Jenkinsfile)的作业——而不是自由式作业。到目前为止,您定义的demo作业只是一个小演示。在此步骤中,您将定义一个更实际的作业,该作业从 GitHub 拉取 Git 存储库并运行在其管道脚本之一中定义的管道。

要让 Jenkins 拉取 Git 存储库并使用管道脚本进行构建,您需要安装额外的插件。因此,在对 Job DSL 脚本进行任何更改之前,请首先确保已安装所需的插件。

导航到your_jenkins_url/pluginManager/installed并检查插件列表中是否存在Git、管道:作业和管道:Groovy插件。如果其中任何一个没有安装,请转到your_jenkins_url/pluginManager/available并搜索并选择插件,然后单击 Install without restart。

现在已经安装了所需的插件,让我们将重点转移到修改 Job DSL 脚本以包含额外的管道作业。

我们将定义一个管道作业,从公共jenkinsci/pipeline-examplesGit 存储库中提取代码并运行其中找到的environmentInStage.groovy声明性管道脚本。

再次导航到Jenkins Job DSL API Reference,单击漏斗图标以调出 Filter by Plugin 菜单,然后取消选择除 Git、Pipeline: Job 之外的所有插件 和 管道:Groovy。

之外的所有插件均已取消选择

单击左侧菜单上的 pipelineJob 并展开pipelineJob(String name) { … }块,然后依次展开definition { … }、cpsScm { … }和scm { … }块。

每个 API 方法上方都有注释,解释了它们的作用。对于我们的用例,您希望使用 GitHub 存储库中的管道脚本来定义管道作业。所以你需要修改你的 Job DSL 脚本,如下所示:

复制代码
 job('demo') {

    
     steps {
    
     shell('echo Hello World!')
    
     }
    
 }
    
  
    
 pipelineJob('github-demo') {
    
     definition {
    
     cpsScm {
    
         scm {
    
             git {
    
                 remote {
    
                     github('jenkinsci/pipeline-examples')
    
                 }
    
             }
    
         }
    
         scriptPath('declarative-examples/simple-examples/environmentInStage.groovy')
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

要进行更改,请转到your_jenkins_url/job/seed/configure并找到 DSL Script 文本区域,并将内容替换为新的 Job DSL 脚本。然后按保存。在下一个屏幕中,单击 Build Now 以重新运行种子作业。

然后,进入新构建的控制台输出页面,你会发现Added items: GeneratedJob{name='github-demo'},这意味着你已经成功添加了新的管道作业,而现有的作业保持不变。

您可以前往your_jenkins_url进行确认;您会发现github-demo作业出现在作业列表中。

最后,通过导航到your_jenkins_url/job/github-demo/并单击 Build Now 来确认您的工作正在按预期工作。构建完成后,导航到your_jenkins_url/job/github-demo/1/console,您将找到控制台输出页面,显示 Jenkins 已成功克隆存储库并执行了管道脚本。

至此,您已使用 Job DSL 插件以一致且可重复的方式在 Jenkins 服务器上配置作业。

但 Job DSL 并不是 Jenkins 生态系统中唯一遵循一切即代码 (EaC) 范式的工具。您还可以将 Jenkins 部署为 Docker 容器并使用 Jenkins 配置即代码 (JCasC)进行设置。 Docker、JCasC、Job DSL 和管道一起使开发人员和管理员能够完全自动地部署和配置 Jenkins,而无需任何手动参与。

更多详情,请参考: 配置即代码

完整的 DSL API 参考在您的 Jenkins 安装中可用,网址为 https://your.jenkins.installation/plugin/job-dsl/api-viewer/index.html。您可以在种子作业页面和作业 DSL 构建步骤上找到指向 API 参考的链接。

API 参考的有限子集可在Jenkins Job DSL Plugin在线获取。但请注意,这并没有显示 Jenkins 安装中可用的所有 API,因为很多文档是在运行时通过检查已安装的插件生成的。

Jenkins 将每个作业的配置保存在一个 XML 文件中。Job DSL 插件原则上是这些 XML 文件的生成器,将 DSL 代码转换为 XML。如果高级 DSL 中的配置选项不可用,则可以使用 Configure Block直接生成 XML 。使用 Job DSL Playground创建和测试您的配置块。

六、构建触发器

前面我们都是在推送代码后,再切换到Jenkins界面,手动触发构建的。显然,这不够“自动化”。自动化是指pipeline按照一定的规则自动执行。而这些规则被称为pipeline触发条件。

对于pipeline触发条件,从两个维度来区分:时间触发和事件触发。

接下来,我们从这两个维度分别介绍。

1、时间触发

时间触发是指定义一个时间,时间到了就触发pipeline执行。在Jenkins pipeline中使用trigger指令来定义时间触发。
tigger指令只能被定义在pipeline块下,Jenkins内置支持cron、pollSCM,upstream三种方式。其

他方式可以通过插件来实现。

1. 定时执行:cron

定时执行就像cronjob,一到时间点就执行。它的使用场景通常是执行一些周期性的job,如每夜构建。

复制代码
 pipeline {

    
     agent any 
    
     triggers {
    
     cron('0 0 * * *')
    
     } 
    
     stages {
    
     stage('Nightly build') {
    
         steps {
    
             echo '这是一个耗时的构建,每天凌晨执行'
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

Jenkins trigger cron语法采用的是UNIX cron语法(有些细微的区别)。一条cron包含5个字段,使用空格或Tab分隔,格式为:MINUTE HOUR DOM MONTH DOW。

每个字段的含义为:

  • MINUTE:一小时内的分钟,取值范围为0∼59。
  • HOUR:一天内的小时,取值范围为0∼23。
  • DOM:一个月的某一天,取值范围为1∼31。
  • MONTH:月份,取值范围为1∼12。
  • DOW:星期几,取值范围为0∼7。0和7代表星期天。

还可以使用以下特殊字符,一次性指定多个值。

  • *:匹配所有的值
  • M-N:匹配M 到N 之间的值。
  • M-N/X or*/X:指定在M 到N 范围内,以X值为步长。
  • A,B,· · ·,Z:使用逗号枚举多个值。

在一些大型组织中,会同时存在大量的同一时刻执行的定时任务,比如N 个半夜零点(0 0***)执行的任务。这样会产生负载不均衡。在Jenkins trigger cron语法中使用“H”字符来解决这一问题,H代表hash。对于没必要准确到零点0分执行的任务,cron可以这样写:H 0***,代表在零点0分至零点59分之间任何一个时间点执行。

需要注意的是,H应用在DOM(一个月的某一天)字段时会有不准确的情况,因为10月有31天,而2月却是28天。

Jenkins trigger cron还设计了一些人性化的别名:@yearly、@annually、@monthly、@weekly、@daily、@midnight和@hourly。

例如,@hourly与H****相同,代表一小时内的任何时间;@midnight实际上代表在半夜12:00到凌晨2:59之间的某个时间。其他别名很少有应用场景。

2. 轮询代码仓库:pollSCM

轮询代码仓库是指定期到代码仓库询问代码是否有变化,如果有变化就执行。大家可能会问:那多久轮询一次?

越频繁越好。因为构建的间隔时间越长,在一次构建内就可能会包含多次代码提交。当构建失败时,你无法马上知道是哪一次代码提交导致了构建失败。

总之,越不频繁集成,得到的“持续集成”的好处就越少。

通常会在Jenkinsfile中这样写:

复制代码
 pipeline {

    
     agent any 
    
     triggers {
    
     // 每分钟判断一次代码是否有变化
    
     pollSCM('H/1 * * * *')
    
     }
    
 }
    
    
    
    
    AI助手

事实上,如果代码有变化,最好的方式是代码仓库主动通知Jenkins,而不是Jenkins频繁去代码仓库检查。那这种方式存在的意义是什么?

在一些特殊情况下,比如外网的代码仓库无法调用内网的Jenkins,或者反过来,则会采用这种方式。

2、事件触发

事件触发就是发生了某个事件就触发pipeline执行。这个事件可以是你能想到的任何事件。比如手动在界面上触发、其他job主动触发、HTTP API Webhook触发等。

1. 由上游任务触发:upstream

当B任务的执行依赖A任务的执行结果时,A就被称为B的上游任务。在Jenkins 2.22及以上版本中,trigger指令开始支持upstream类型的触发条件。upstream的作用就是能让B pipeline自行决定依赖哪些上游任务。

示例如下:

复制代码
 // job1 和 job2都是任务名

    
 triggers {
    
     upstream(upstreamProjects:'job1,job2', threshold: hudson.model.Result.SUCCESS)
    
 }
    
    
    
    
    AI助手

当upstreamProjects参数接收多个任务时,使用,分隔。threshold参数是指上游任务的执行结果是什么值时触发。

hudson.model.Result是一个枚举,包括以下值:

  • ABORTED:任务被手动中止。
  • FAILURE:构建失败。
  • SUCCESS:构建成功。
  • UNSTABLE:存在一些错误,但不至于构建失败。
  • NOT_BUILT:在多阶段构建时,前面阶段的问题导致后面阶段无法执行。

注意:需要手动触发一次任务,让Jenkins加载pipeline后,trigger指令才会生效。

2. 在pipeline中实现GitLab trigger

首先需要在Jenkins界面上手动勾选“Build when a change is pushed to GitLab”复选框,还需要手动单击“Generate”按钮生成Secret token或手动输入Secret token。

这部分手动操作,我们也希望通过修改Jenkinsfile就能实现。显然,不只是我们这样想,早就有人想到了,并在GitLab插件上实现了基于GitLab的trigger。

以下是具体使用方法:

复制代码
 pipeline {

    
     agent any 
    
     triggers {
    
     gitlab(triggerOnPush: true,
    
            triggerOnMergeRequest: true, 
    
            branchFilterType: 'All',
    
            secretToken: "abcdefghijklmnopqrstuvwxyzo")
    
     }
    
     stages {
    
     stage('build') {
    
         steps {
    
             echo "hello world from gitlab trigger"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

secretToken使用随机字符串生成器生成即可。如果Jenkins在内网使用,并且安全性有一定的保障,我们可以将secretToken定义为一个Jenkins全局变量,供所有的项目使用。这样做就不用为每个项目重新生成token了。

GitLab trigger方法有很多参数可配置,下面简单介绍一些常用的参数。

  • triggerOnPush:当GitLab触发push事件时,是否执行构建。
  • triggerOnMergeRequest:当GitLab触发mergeRequest事件时,是否执行构建。
  • branchFilterType:只有符合条件的分支才会被触发。必选,否则无法实现触发。可以设置的值有:
  1. NameBasedFilter:基于分支名进行过滤,多个分支名使用逗号分隔。
  2. RegexBasedFilter:基于正则表达对分支名进行过滤。
  3. All:所有分支都会被触发。
  • includeBranchesSpec:基于branchFilterType值,输入期望包括的分支的规则。
  • excludeBranchesSpec:基于branchFilterType值,输入期望排除的分支的规则。

想了解更多的参数,可以在GitLab plugin的GitHub页面中查看。

3. 将构建状态信息推送到GitLab

当Jenkins执行完构建后,我们还可以将构建结果推送到GitLab的相应commit记录上,这样就可以将构建状态与commit关联起来。方法如下:

(1)进入Jenkins→Configure System页,找到“Gitlab”选项,填入GitLab地址。

注意“Connection name”的值,后面会使用到。

(2)根据提示,我们需要设置它的Credentials(凭证)。单击“Add”按钮,在弹出的对话框中输入之前保存的API token,单击“Add”按钮确定添加。

在Credentials下拉列表中选择“GitLab API token”后,单击“Test Connection”按钮,如果返回Success,就说明集成成功了。

(3)在pipeline的post部分,将构建结果更新到GitLab的相应commit记录上。

除此之外,还需要在options部分加入gitLabConnection配置,同时传入“gitlab”参数。

“gitlab”就是上文中提醒大家注意的“Connection name”的值。

复制代码
 pipeline {

    
     agent any 
    
     triggers {
    
     gitlab(triggerOnPush: true, triggerOnMergeRequest: true, branchFilterType: 'All', secretToken: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEF")
    
     }
    
     stages {
    
     stage('build') {
    
         steps {
    
             echo "hello world from gitlab trigger"
    
         }
    
     }
    
     }
    
     post {
    
     failure {
    
         updateGitlabCommitStatus name: 'build',state: 'failed'
    
     }
    
     success {
    
         updateGitlabCommitStatus name: 'build',state:'success'
    
     }
    
     }
    
     options {
    
     gitLabConnection('gitlab')
    
     }
    
 }
    
    
    
    
    AI助手

提交代码后,需要手动触发一次构建,pipeline才会生效。

这里做了一次成功构建、一次失败构建的实验。在GitLab上的项目的commit列表中,显示了最近两次commit的构建状态。

4. 使用Generic Webhook Trigger插件实现触发

安装GitLab插件后,GitLab系统就可以发送Webhook触发Jenkins项目的执行。那是不是说其他系统想触发Jenkins项目执行,也需要找一个插件或者开发一个插件来实现呢?

有了Generic Webhook Trigger插件(https://plugins.jenkins.io/generic-webhook-trigger)就不需要了。

安装 Generic Webhook Trigger 插件(下文使用 GWT 简称)后,Jenkins 会暴露一个 API:<JENKINS URL>/generic-webhook-trigger/invoke,即由GWT插件来处理此API的请求。

如何处理呢?

GWT插件接收到JSON或XML的HTTP POST请求后,根据我们配置的规则决定触发哪个Jenkins项目。基本原理就这么简单。下面我们先感受一下,然后再详细介绍GWT各参数的含义。

现在,我们创建一个普通的pipeline项目。

代码如下:

复制代码
 pipeline {

    
     agent any 
    
     triggers {
    
     GenericTrigger(
    
     genericVariables:[
    
         [key: 'ref', value:'$.ref']
    
     ],
    
     token: 'secret',
    
     causeString: 'Triggered on $ref', 
    
     printContributedVariables: true, 
    
     printPostContent: true 
    
     )
    
     }
    
     stages {
    
     stage('Some step') {
    
         steps {
    
             sh "echo $ref"
    
             sh "printenv"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

注意:在创建完成后,需要手动运行一次,这样pipeline的触发条件才会生效。

然后,我们发起一次HTTP POST请求。

复制代码
    curl -× POST -H "Content-Type: application/json"  -d '{ "ref":"refs/heads/master" }' -vs http://192.168.23.11:8667/jenkins/generic-webhook-trigger/invoke?token=secret
    
    
    AI助手

接着,我们就看到pipeline被触发执行了。

日志如下:

复制代码
 Triggered on refs/heads/master

    
 ...省略git操作细节部分
    
 Seen branch in repository origin/dev 
    
 Seen branch in repository origin/master 
    
 Seen 2 remote branches
    
  
    
 Obtained Jenkinsfile from 6b2c6a36ac5fade7448596a5fc67ee33be4bab95 
    
 Running in Durability level:MAX_SURVIVABILITY 
    
 GenericWebhookEnvironmentContributor 
    
     Received:
    
 { "ref": "refs/heads/master" }
    
  
    
 Contributing variables:
    
     ref = refs/heads/master
    
    
    
    
    AI助手

GenericTrigger触发条件由GWT插件提供。此触发条件可以说是GWT的所有内容。

GenericTrigger触发条件分为5部分,这样更易于理解各参数的作用。

  • 从HTTP POST请求中提取参数值。
  • token,GWT插件用于标识Jenkins项目的唯一性。
  • 根据请求参数值判断是否触发Jenkins项目的执行。
  • 日志打印控制。
  • Webhook响应控制。

1)从Webhook请求中提取参数值

一个HTTP POST请求可以从三个维度提取参数,即POST body、URL参数和header。GWT插件提供了三个参数分别对这三个维度的数据进行提取。

(1)genericVariables:提取POST body中的参数。

复制代码
 genericVariables: [

    
     [key: 'ref', value: '$.ref'], 
    
     [key:'before',
    
      value: '$.before',
    
      expressionType: 'JSONPath',
    
      regexpFilter: '',
    
      defaultValue: ''
    
     ]
    
 ]
    
    
    
    
    AI助手
  • value:JSONPath表达式,或者XPath表达式,取决于expressionType参数值,用于从POST body中提取值。
    • key:从POST body中提取出的值的新变量名,可用于pipeline其他步骤。
    • expressionType:可选,value的表达式类型,默认为JSONPath。当请求为XML内容时,必须指定XPath值。
    • defaultValue:可选,当提取不到值,且defaultValue不为空时,则使用defaultValue作为返回值。
    • regexpFilter:可选,过滤表达式,对提取出来的值进行过滤。regexpFilter做的事情其实就是string.replaceAll(regexpFilter,"");。string是从HTTP请求中提取出来的值。

(2)genericRequestVariables:从URL参数中提取值。

复制代码
 genericRequestVariables:[

    
     [key:'requestwithNumber', regexpFilter:'[^0-9]'],             
    
     [key:'requestwithString', regexpFilter:'']
    
 ]
    
    
    
    
    AI助手
  • key:提取出的值的新变量名,可用于pipeline其他步骤。
    • regexpFilter:对提取出的值进行过滤。

(3)genericHeaderVariables:从HTTP header中提取值。

复制代码
 genericHeaderVariables: [

    
     [key: 'headerwithNumber', regexpFilter: '[^0-9]'], 
    
     [key: 'headerWithString', regexpFilter: '']
    
 ]
    
    
    
    
    AI助手

genericHeaderVariables的用法与genericRequestVariables一样,区别是它是从HTTP header中提取值的。

2)触发具体某个Jenkins项目

前面我们看到GenericTrigger方法有一个token参数。

复制代码
 triggers {

    
     GenericTrigger(
    
     token: 'secret',
    
     )
    
 }
    
    
    
    
    AI助手

token参数的作用是标识一个pipeline在Jenkins中的唯一性(当然,没有人阻止你让所有的pipeline使用同一个token)。为什么需要这个参数呢?这要从GWT插件的原理说起。

当Jenkins接收到generic-webhook-trigger/invoke接口的请求时,会将请求代理给GWT插件处理。GWT插件内部会从Jenkins实例对象中取出所有的参数化Jenkins项目,包括pipeline,然后进行遍历。如果在参数化项目中GenericTrigger配置的token的值与Webhook请求时的token的值一致,则触发此参数化项目。

如果多个参数化项目的token值一样,则它们都会被触发。

小技巧:pipeline的token可以被设置为Jenkins的项目名。比如:

复制代码
 triggers {

    
     GenericTrigger(
    
     // 省略
    
     token: env.JOB_NAME, 
    
     // 省略
    
     )
    
 }
    
    
    
    
    AI助手

3)根据请求参数值判断是否触发Jenkins项目执行

GWT并不只是根据token值来判断是否触发,还可以根据我们提取出的值进行判断。

示例如下:

复制代码
 GenericTrigger(

    
     genericVariables: [
    
     [key: 'refValue', value: '$.ref']
    
     ],
    
     token: env.JOB_NAME, 
    
     regexpFilterText: '$refValue',
    
     regexpFilterExpression: 'refs/heads/(master|dev)'
    
 )
    
    
    
    
    AI助手
  • regexpFilterText:需要进行匹配的key。例子中,我们使用从POST body中提取出的refValue变量值。
    • regexpFilterExpression:正则表达式。

如果regexpFilterText参数的值符合regexpFilterExpression参数的正则表达式,则触发执行。

4)控制打印内容

打印日志有助于调试,GWT插件提供了三个参数。

  • printPostContent:布尔值,将Webhook请求信息打印到日志上。
  • printContributedVariables:布尔值,将提取后的变量名及变量值打印出来。
  • causeString:字符串类型,触发原因,可以直接引用提取后的变量,如 causeString:'Triggered on $msg'。

5) 控制响应

GWT插件新增了一个参数:

  • silentResponse:布尔类型,在正常情况下,当Webhook请求成功后,GWT插件会返回HTTP 200状态码和触发结果给调用方。但是当silentResponse设置为true时,就只返回HTTP 200状态码,不返回触发结果。

七、并行分布式构建

假如单机足够强大,让更多项目同时执行的方法就是增加executor。但单机的容量总会遇到上限,而且还会有单节点问题。

解决办法就是将Jenkins项目分配到多台机器上执行,这就是分布式构建。

1、增加agent

实现分布式构建最常用、最基本的方式就是增加agent。Jenkins agent作为一个负责执行任务的程序,它需要与Jenkins master建立双向连接。连接方式有多种,这也代表有多种增加agent的方式。

在真正介绍如何增加agent前,我们需要了解标签(label)在分布式构建中的作作用。

1. 对agent打标签

当agent数量变多时,如何知道哪些agent支持JDK 8、哪些agent支持Node.js环境呢?我们可以通过给agent打标签(有时也称为tag)来确定。

通过标签将多个agent分配到同一个逻辑组中,这个过程被称为打标签。同一个agent可以拥有多个标签。在标签名中不能包含空格,也不能包含!、&、|、<、>、(、)这些特殊字符中的任何一个。因为包含特殊字符的标签名与标签表达式(用于过滤agent)冲突。

对于支持JDK 8的agent,我们打上jdk8标签;对于支持Node.js的agent,我们打上nodejs标签;如果一个agent同时支持JDK 8和Node.js,那么就两个标签都打上。

在打标签时,可以根据以下几个维度来进行。

  • 工具链:jdk、nodejs、ruby;也可以加上工具的版本,如jdk6、jdk8。
  • 操作系统:linux、windows、osx;或者加上操作系统的版本,如ubuntu18.04、centos7.3。
  • 系统位数:32bit、64bit。

可以根据实际项目情况新增维度,对于不同的增加agent的方式,打标签的方式也不同。

2. agent部分详解

打完标签后,如何在pipeline中使用标签呢?

agent部分描述的是整个pipeline或在特定阶段执行任务时所在的agent。换句话说,Jenkins master根据此agent部分决定将任务分配到哪个agent上执行。agent部分必须在pipeline块内的顶层定义,而stage块内的定义是可选的。

  • any

到此为止,pipeline的agent部分都是这样写的:

复制代码
 pipeline {

    
     agent any
    
     //... 省略其他部分
    
 }
    
    
    
    
    AI助手

agent any告诉Jenkins master任何可用的agent都可以执行。

agent部分的定义可以放在阶段中,用于指定该stage执行时的agent。

复制代码
 pipeline {

    
     agent any // 不能省略
    
     stages {
    
     stage('Build') {
    
         agent any 
    
         steps {
    
             echo "build"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

注意:pipeline块内的agent部分是必需的,不能省略。

  • 通过标签指定agent

当pipeline需要在JDK 8环境下进行构建时,就需要通过标签来指定agent。

代码如下:

复制代码
 pipeline {

    
     agent {
    
     label 'jdk8'
    
     }
    
     stages {
    
     stage('Build') {
    
         steps {
    
             echo "build"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

事实上,agent {label'jdk8'}是以下定义方式的简略写法。

复制代码
 agent {

    
     node {
    
     label 'jdk8'
    
     }
    
 }
    
    
    
    
    AI助手

有些构建任务是需要在JDK 8及Windows环境下执行的。也就是说,我们需要过滤同时具有windows和jdk8标签的agent。

可以这样写:

复制代码
 agent {

    
     label 'windows && jdk8'
    
 }
    
    
    
    
    AI助手

使用&&代表并且关系。

上文中,在增加agent时,已经配置好了该agent上的默认工作目录路径,但是agent部分允许我们对工作目录进行自定义。

node除了label选项,还提供了另一个选项——customWorkspace,自定义工作目录,写法如下:

复制代码
 agent {

    
     node {
    
     label 'jdk8'
    
     customWorkspace '/var/lib/custom'
    
     }
    
 }
    
    
    
    
    AI助手

customWorkspace选项除了写绝对路径,还可以写相对于默认工作目录路径的相对路径。

  • 不分配节点

以上介绍的是如何分配agent,其实还可以指定不分配agent,写法很简单:agent none,指的是不分配任何agent。

没有真正遇到过使用场景,可能就很难想象在什么时候使用agent。如果希望每个stage都运行在指定的agent中,那么pipeline就不需要指定agent了。

示例如下:

复制代码
 pipeline {

    
     agent none 
    
     stages {
    
     stage('Example Build') {
    
         agent { label 'mvn' }
    
         steps { 
    
             echo 'Hello, build'
    
         }
    
         stage('Example Test') {
    
             agent { label 'test' }
    
             steps {
    
                 echo 'Hello, test'
    
             }
    
         }
    
     }
    
     }
    
 }
    
  
    
  
    
    
    
    
    AI助手
  • when指令的beforeAgent选项

在默认情况下,阶段内所有的代码都将在指定的Jenkins agent上执行。when指令提供了一个beforeAgent选项,当它的值为true时,只有符合when条件时才会进入该Jenkins agent。这样就可以避免没有必要的工作空间的分配,也就不需要等待可用的Jenkins agent了。

在某些场景下,beforeAgent选项通常用于加速pipeline的执行。

示例如下:

复制代码
 pipeline {

    
     agent none 
    
     stages {
    
     stage('Example Build') {
    
         steps {
    
             echo 'Hello World'
    
         }
    
     }
    
     stage('Example Deploy') {
    
         agent {
    
             label "some-label"
    
         }
    
         when {
    
             beforeAgent true 
    
             branch 'production'
    
         }
    
         steps {
    
             echo 'Deploying'
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

只有分支为production时,才会进入“Example Deploy”阶段。这样就可以避免在some-label的agent中拉取代码,从而达到加速pipeline执行的目的。

2、将构建任务交给Docker

目前我们所有的构建都执行在机器(物理机器或虚拟机器)上,这里我们让构建执行在Docker容器中。

Jenkins master要将构建任务分配给Docker,就必须在Jenkins agent上安装Docker,建议给这些agent打上docker的标签。

1. 在Jenkins agent上安装Docker

关于 Docker 的具体安装过程就不细表了。但是需要注意,要将 Jenkins agent 的用户加入Docker的用户组中,这样Jenkins agent不需要加sudo就能执行docker命令。

如果不生效,则可能需要重启Jenkins agent。

2. 使用Docker

pipeline插件从2.5版本开始就内置了Docker插件(https://plugins.jenkins.io/docker-plugin)。所以,安装pipeline插件后,就可以直接在pipeline的agent部分指定使用什么Docker镜像进行构建了。

复制代码
 pipeline {

    
     agent {
    
     docker {
    
         label 'docker'
    
         image 'maven:3-alpine'
    
     }
    
     stages {
    
         stage('build') {
    
             steps {
    
                 sh 'mvn clean compile' 
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

与之前不同的,在agent部分我们将node换成了docker,下面分别解释docker的常用选项。

  • label(可选):字符串类型,与node的label的作用一样。
  • image:字符串类型,指定构建时使用的Docker镜像。
  • args(可选):字符串类型,Jenkins执行docker run命令时所带的参数,如args'-v/tmp:/tmp'。
  • alwaysPull(可选):布尔类型,强制每次执行docker pull命令时都重新拉取镜像。

Docker拉取镜像时,默认是从Docker官方中心仓库拉取的,如何实现从私有仓库拉取呢?

Docker插件为我们提供了界面操作,具体步骤如下:

进入Manage Jenkins→Configure System页面,找到“Pipeline Model Definition”部分。

  • Docker Label:当 pipeline 中的 agent 部分没有指定 label 选项时,就会使用此配置。如docker {image'maven:3-alpine'}。
  • Docker registry URL:Docker私有仓库地址。
  • Registry credentials:登录Docker私有仓库的凭证。

3、并行构建

如果需要分别在Chrome、Firefox、IE等浏览器的各个不同版本中对同一个Web应用进行UI测试,该怎么做呢?

根据前文对Jenkins pipeline的学习,我们也许会这样写:

复制代码
 pipeline {

    
     agent any
    
     stages {
    
     stage('Build') {
    
         steps {
    
             echo 'Building..' 
    
         }
    
     }    
    
     stage('Test on Chrome') {
    
         steps {
    
             echo 'Testing..' 
    
         }
    
     }
    
     stage('Test on Firfox') {
    
         steps {
    
             echo 'Testing..' 
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

不论是将UI分别放在不同的阶段还是步骤中,这种按顺序执行的测试都太慢了,慢到执行一次完整的UI测试要一小时以上。

通过仔细分析你会发现,这些测试是可以并行执行的。就像原来只有一个测试人员,要测试4个浏览器,他只能测试完一个浏览器,再测试另一个浏览器,但是现在有4个测试人员,他们就可以同时进行测试。这样测试耗时就降到了原来的1/4。

很明显,Jenkins pipeline插件支持这种并行构建,并且使用起来也非常简单。

复制代码
 pipeline {

    
     agent none 
    
     stages {
    
     stage('Run Tests') {
    
         failFast true 
    
         parallel {
    
             stage('Test On Chrome') {
    
                 agent { label "chrome" }
    
                 steps {
    
                     echo "Chrome UI测试"
    
                 }
    
                 stage('Test On Firefox') {
    
                     agent { label "firefox" }
    
                     steps {
    
                         echo "Firefox UI测试"
    
                     }
    
                 }
    
                 stage('Test On IE') {
    
                     agent { label "ie" }
    
                     steps {
    
                         echo "IE UI测试"
    
                     }
    
                 }
    
             } 
    
         } // end of parallel
    
     } // end of run tests
    
    } // end of stages 
    
 }// end of pipeline
    
    
    
    
    AI助手

在stages部分包含一个Run Tests阶段(限于篇幅,我们只写这个阶段),在这个阶段下包含一个parallel块,在parallel块下又包含了多个阶段。位于parallel块下的阶段都将并行执行,而且并行阶段还可以被分到不同的Jenkins agent上执行。

因为parallel本身不包含任何步骤,所以在parallel块下本身不允许包含agent和tools。

在默认情况下,Jenkins pipeline要等待parallel块下所有的阶段都执行完成,才能确定结果。如果希望所有并行阶段中的某个阶段失败后,就让其他正在执行的阶段都中止,那么只需要在与parallel块同级的位置加入failFast true就可以了。

1. 在不同的分支上应用并行构建

并行构建不仅可以被应用在UI自动化测试中,还可以被应用在不同的分支上。

复制代码
 pipeline {

    
     agent any 
    
     stages {
    
     stage('Parallel Stage') {
    
         failFast true 
    
         parallel {
    
             stage('Branch master') {
    
                 when { branch 'master' }
    
                 agent any 
    
                 steps {
    
                     echo "On Branch master"
    
                 }
    
             }
    
             stage('Branch dev') {
    
                 when { branch 'dev' }
    
                 agent any 
    
                 steps {
    
                     echo "On Branch dev"
    
                 }
    
             }
    
             stage('Branch staging') {
    
                 when { branch 'staging' }
    
                 agent any 
    
                 stages {
    
                     stage('嵌套staging 1') {
    
                         steps {
    
                             echo "staging 1"
    
                         }
    
                     }
    
                     stage('嵌套staging 2') {
    
                         steps {
    
                             echo "staging 2"
    
                         }
    
                     }
    
                 }
    
             } // end of staging 
    
         } // end of parallel 
    
     }
    
     }
    
 } // end of pipepline
    
    
    
    
    AI助手

我们注意到在并行阶段Branch staging下又出现了一个stages部分。是的,阶段是可以嵌套的。但是可以嵌套多少层呢?

Jenkins的文档并没有明确说明。建议不要超过三层,因为在同一个Jenkins pipeline中实现过于复杂的逻辑,说明Jenkins pipeline的职责不够单一,需要进行拆分。

2. 并行步骤

前面我们介绍的是阶段级别的并行执行,Jenkins pipeline还支持步骤级别的并行执行。这也是Jenkins pipeline最早支持的并行方式。

复制代码
 stage('并行构建') {

    
     steps {
    
     parallel(
    
         jdk8: {
    
             echo "jdk8 build"
    
         },
    
         jdk9: {
    
             echo "jdk9 build"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

除了写法的不同,表面上看并行阶段与并行步骤并没有太大的区别。

但是它们有一个关键的区别:并行阶段运行在不同的executor上,而并行步骤运行在同一个executor上。这样看来其实就是并行与并发的区别。并行步骤本质上是并发步骤。

八、参数化构建

参数化pipeline是指可以通过传参来决定pipeline的行为。参数化让写pipeline就像写函数,而函数意味着可重用、更抽象。所以,通常使用参数化pipeline来实现一些通用的pipeline。

1、parameters 指令

在Jenkins pipeline中定义参数使用的是parameters指令,其只允许被放在pipeline块下。

代码如下:

复制代码
 pipeline {

    
     agent any
    
     parameters {
    
     booleanParam(defaultvalue: true, description: '', name:'userFlag')
    
     }
    
     stages {
    
     stage("foo") {
    
         steps {
    
             echo "flag: ${params.userFlag}"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

booleanParam方法用于定义一个布尔类型的参数。booleanParam方法接收三个参数。

  • defaultValue:默认值。
  • description:参数的描述信息。
  • name:参数名。

在定义了pipeline的参数后,如何使用呢?

被传入的参数会放到一个名为params的对象中,在pipeline中可直接使用。params.userFlag就是引用parameters指令中定义的userFlag参数。

值得注意的是,在Jenkins新增此pipeline后,至少要手动执行一次,它才会被Jenkins加载生效。生效后,在执行项目时,就可以设置参数值了。

为了满足不同的应用场景,参数化pipeline支持多种参数类型,包括:

  • string,字符串类型。
复制代码
 parameters {

    
     string(name: 'DEPLOY_ENV', defaultValue: 'staging', description: '')
    
 }
    
    
    
    
    AI助手
  • text,多行文本类型,换行使用\n。
复制代码
 parameters {

    
     text(name: 'DEPLOY_TEXT', defaultValue: 'One\nTwo\nThree\n', description: '')
    
 }
    
    
    
    
    AI助手
  • booleanParam,布尔类型。
复制代码
 parameters {

    
     booleanParam(name: 'DEBUG_BUILD', defaultValue: true, description: '')
    
 }
    
    
    
    
    AI助手
  • choice,选择参数类型,使用\n来分隔多个选项。
复制代码
 parameters {

    
     choice(name:'CHOICES',choices:'dev\ntest\nstaging',description:'请选择部署的环境')
    
 }
    
    
    
    
    AI助手
  • file,文件类型,用户可上传文件。

但是此类型存在Bug——你无法拿到上传后的文件,所以不推荐使用。具体可以看官方issue:JENKINS-27413。

  • password,密码类型。
复制代码
 parameters {

    
     password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'A secret password')
    
 }
    
    
    
    
    AI助手

当然,参数化的pipeline不可能只支持接收一个参数,以下就是为pipeline同时定义多个参数的例子。

复制代码
 parameters {

    
     booleanParam(name: 'DEBUG_BUILD', defaultValue: true, description: '')
    
     password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'A secret password')
    
     string(name: 'DEPLOY_ENV', defaultValue: 'staging', description: '')
    
 }
    
    
    
    
    AI助手

2、由另一个pipeline传参并触发

既然存在参数化的pipeline,那么就表示可以在一个pipeline中“调用”另一个pipeline。在Jenkins pipeline中可以使用build步骤实现此功能。build步骤是pipeline插件的一个组件,所以不需要另外安装插件,可以直接使用。

build步骤其实也是一种触发pipeline执行的方式,它与triggers指令中的upstream方式有两个区别:

(1) build步骤是由上游pipeline使用的,而upstream方式是由下游pipeline使用的。

(2) build步骤是可以带参数的,而upstream方式只是被动触发,并且没有带参数。

假如调用本章开头的例子,我们可以在steps部分这样写:

复制代码
 steps {

    
     build(
    
     job:"parameters-example", 
    
     parameters: [
    
         booleanParam(name:'userFlag',value:true)
    
     ]
    
     )
    
 }
    
    
    
    
    AI助手

我们来看看build步骤的基本的两个参数。

  • job(必填):目标Jenkins任务的名称。
  • parameters(可选):数组类型,传入目标pipeline的参数列表。传参方法与定参方法类似。
复制代码
 parameters: [

    
     booleanParam(name: 'DEBUG_BUILD', value: true),
    
     password(name: 'PASSWORD', value: prodSECRET'),
    
     string(name: 'DEPLOY_ENV', value: 'prod'),
    
     text(name: 'DEPLOY_TEXT', value: 'a\nb\nc\n'), 
    
     string(name: 'CHOICES', value: 'dev')
    
 ]
    
 
    
    
    AI助手

我们注意到choice类型的参数没有对应的传参方法,而是使用string传参方法代替的。

除此之外,build步骤还支持其他三个参数。

  • propagate(可选):布尔类型,如果值为true,则只有当下游pipeline的最终构建状态为SUCCESS时,上游pipeline才算成功;如果值为false,则不论下游pipeline的最终构建状态是什么,上游pipeline都忽略。默认值为true。
  • quietPeriod(可选):整型,触发下游pipeline后,下游pipeline等待多久执行。如果不设置此参数,则等待时长由下游pipeline确定。单位为秒。
  • wait(可选):布尔类型,是否等待下游pipeline执行完成。默认值为true。

如果你使用了Folder插件(https://plugins.jenkins.io/cloudbees-folder),那么就需要注意build步骤的job参数的写法了。

使用Folder插件,可以让我们像管理文件夹下的文件一样来管理Jenkins项目。我们的Jenkins项目可以创建在这些文件夹下。如果目标pipeline与源pipeline在同一个目录下,则可以直接使用名称;如果不在同一个目录下,则需要指定相对路径,如../sister-folder/downstream,或者指定绝对路径,如/top-level-folder/nested-folder/downstream。

3、Conditional BuildStep插件

有些场景要求我们根据传入的参数做一些逻辑判断。很自然地,我们想到了在script函数内就可以实现。

复制代码
 stage("deploy to test"){

    
     steps{
    
     script {
    
         if (params.CHOICES == 'test') {
    
             echo "deploy to test"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

这样写起来很不优雅,Conditional BuildStep插件(https://plugins.jenkins.io/conditional-buildstep)可以让我们像使用when指令一样进行条件判断。

以下代码就是安装Conditional BuildStep插件后的写法:

复制代码
 pipeline {

    
     agent any
    
     parameters {
    
     choice(name:'CHOICES',choices:'dev\ntest\nstaging',description:'请选择部署的环境')
    
     stages {
    
     stage("deploy test") {
    
         when {
    
             expression { return params.CHOICES == 'test' }
    
         }
    
         steps {
    
             echo "deploy to test"
    
         }
    
     }
    
     stage("deploy staging") {
    
         when {
    
             expression { return params.CHOICES == 'staging' }
    
         }
    
         steps {
    
             echo "deploy to staging"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

现实中,我们会面对更复杂的判断条件。而expression表达式本质上就是一个Groovy代码块,大大提高了表达式的灵活性。以下是比较常用的例子。

  • 或逻辑
复制代码
 when {

    
     // A or B 
    
     expression { return A || B }
    
 }
    
    
    
    
    AI助手
  • 与逻辑
复制代码
 when {

    
     // A and B
    
     expression { return A && B }
    
 }
    
    
    
    
    AI助手
  • 从文件中取值
复制代码
 when {

    
     expression { return readFile('pom.xml').contains('mycomponent') }
    
 }
    
    
    
    
    AI助手
  • 正则表达式
复制代码
 when {

    
     expression { return token ==~ /(?i)(Y|YES|T|TRUE|ON|RUN)/ }
    
 }
    
    
    
    
    AI助手

4、input步骤

执行input步骤会暂停pipeline,直到用户输入参数。这是一种特殊的参数化pipeline的方法。

我们可以利用input步骤实现以下两种场景:

(1)实现简易的审批流程。例如,pipeline暂停在部署前的阶段,由负责人点击确认后,才能部署。
(2)实现手动测试阶段。在pipeline中增加一个手动测试阶段,该阶段中只有一个input步骤,当手动测试通过后,测试人员才可以通过这个input步骤。

1. input步骤的简单用法

接下来,我们介绍input步骤的简单使用方式。

在Jenkinsfile中加入input步骤:

复制代码
 pipeline {

    
     agent any 
    
     stages {
    
     stage('deploy') {
    
         steps {
    
             input message:"发布或停止"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

如果input步骤只有message参数,则这样写更简洁:input "发布或停止",当pipeline执行到deploy阶段后,就会暂停。

2. input步骤的复杂用法

input步骤的简单用法很难满足更复杂的场景。现在,我们来看一种更复杂的使用方式。

复制代码
 // 变量名,用于存储input步骤的返回值

    
 def approvalMap
    
  
    
 pipeline {
    
     agent any 
    
     stages {
    
     stage('pre deploy') {
    
         steps {
    
             script {
    
                 approvalMap = input(
    
                     message:'准备发布到哪个环境?',
    
                     ok:'确定', 
    
                     parameters: [
    
                         choice(choices:'dev\ntest\nprod', description:'发布到什么环境?', name:ENV'),
    
   16.                             string(defaultValue: '', description: '', name: 'myparam')
    
                     ],
    
                     submitter: 'admin,admin2,releaseGroup', 
    
                     submitterParameter:'APPROVER'
    
                 )
    
             }
    
         }
    
     }
    
     stage('deploy') {
    
         steps { 
    
             echo "操作者是 ${approvalMap['APPROVER']}"
    
             echo "发布到什么环境? ${approvalMap['ENV']}"
    
             echo "自定义参数:${approvalMap['myparam']}"
    
         }
    
     }
    
     }
    
 }
    
 
    
    
    AI助手

我们在pipeline外定义了一个变量approvalMap。这是因为定义在阶段内的变量的作用域只在这个阶段中,而input步骤的返回值需要跨阶段使用,所以需要将其定义在pipeline外。这样变量approvalMap的作用域就是整个pipeline了。

同时,由于在pipeline中直接使用了Groovy语言赋值表达式,所以需要将approvalMap=input(...)放到script块中。

input步骤的返回值类型取决于要返回的值的个数。如果只有一个值,返回值类型就是这个值的类型;如果有多个值,则返回值类型是Map类型。

本例返回的approvalMap就是一个map,Map的key就是每个参数的name属性,比如ENV、myparam都是key。

除了可以在返回的map中放手动输入的值,还可以放其他数据,比如submitterParameter:'APPROVER'代表将key APPROVER放到返回的map中。
下面我们分别介绍input步骤的参数。

  • message:input步骤的提示消息。
  • submitter(可选):字符串类型,可以进行操作的用户ID或用户组名,使用“,”分隔,在“,”左右不允许有空格。这在做input步骤的权限控制方面很实用。
  • submitterParameter(可选):字符串类型,保存input步骤的实际操作者的用户名的变量名。
  • ok(可选):自定义确定按钮的文本。
  • parameters(可选):手动输入的参数列表。

approvalMap还有一种定义方式,放在environment中。

复制代码
 environment {

    
     approvalMap = ''
    
 }
    
    
    
    
    AI助手

这样,就不需要在pipeline顶部定义变量了,这种方式是不是更优雅?

在上游pipeline中调用下游pipeline时,可以采用以下做法。

复制代码
 build job: 'all-in-one-deploy', parameters: [

    
     string(name:'DEPLOY_ENV',value:"${deploy_env}"),     
    
     string(name:'triggerJobName',value:"${env.JOB_NAME}"),
    
     string(name:'triggerJobBuildNumber',value:"${env.BUILD_NUMBER}")
    
 ]
    
    
    
    
    AI助手

input步骤可以与timeout步骤实现超时自动中止pipeline,防止无限等待。以下pipeline一小时后不处理就自动中止。

复制代码
 pipeline {

    
     agent any 
    
     stages {
    
     stage('deploy') {
    
         steps {
    
             timeout(time: 1, unit: 'HOURS') {
    
                 input message:"发布或停止"
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

5、参数化构建案例

有时在项目构建的过程中,我们需要根据用户的输入动态传入一些参数,从而影响整个构建结果,这时我们可以使用参数化构建。

下面演示通过输入gitlab项目的分支名称来部署不同分支项目。

步骤如下:

1. 在gitlab上创建了 master、develop两个分支;

2. 设置构建过程参数类型;

3. 引用参数

4. 开始构建

Jenkinsfile:

完整代码:

复制代码
 pipeline {

    
   agent any
    
   parameters {
    
     booleanParam(name: 'ENABLE_BACKEND_BUILD', defaultValue: true, description: '开启后端构建')
    
     booleanParam(name: 'ENABLE_DEPLOY', defaultValue: true, description: '开启部署')
    
   }
    
   stages {
    
     stage('初始化') {
    
     steps {
    
         echo '初始化。。。'
    
     }
    
     }
    
     stage('打包') {
    
     when{
    
         expression{params.ENABLE_BACKEND_BUILD}
    
     }
    
     steps {
    
         echo '打包。。。'
    
         ws("backend_build"){
    
             dir("my_docker"){
    
                 echo 'git代码拉取。。。'
    
                 checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [], userRemoteConfigs: [[credentialsId: 'a8db6793-cc2b-4d82-bd3d-c5beb1c5149e', url: 'http://192.168.3.11/root/rapid-screen.git']]])
    
         
    
                 echo 'mvn打包。。。'
    
                 sh "/usr/local/apache-maven-3.8.2/bin/mvn -U clean install"
    
             }
    
             echo '打包完成。。。'
    
             // 删除backend_build工作目录
    
             cleanWs()
    
         }
    
     }
    
     }
    
     stage('测试') {
    
     steps {
    
         echo '测试。。。'
    
     }
    
     }
    
     stage('发布') {
    
     when{
    
         expression{params.ENABLE_DEPLOY}
    
     }
    
     steps {
    
         echo '发布。。。'
    
     }
    
     }
    
   }
    
 }
    
    
    
    
    AI助手

九、构建通知

1、邮箱通知

1. 安装插件 Email Extension Template、Email Extension

2. 开启邮箱的 POP3/SMTP服务

在这里我使用 163邮箱作为Jenkins的系统邮箱,即发件邮箱。发件邮箱需要开启 POP3/SMTP服务。

下面为开启 163邮箱的 POP3/SMTP服务 的步骤。

(1)登录网易邮箱:163网易免费邮-你的专业电子邮局

(2)开启 POP3/SMTP服务 服务。

(3)开启后会有一个授权码,记得复制并保存下来。

3. Jenkins设置邮箱相关参数

(1)系统管理 -> 系统配置 -> Jenkins Location。

(2)配置Extended E-mail Notification。

复制代码
 ${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS} !

    
  
    
 ${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS} !
    
  
    
 Check console output at $BUILD_URL to view the results.
    
    
    
    
    AI助手

(3)配置邮件邮件通知。

(4)测试是否配置成功。

4. 编写邮件模板

src/main/resources/email.html:

复制代码
 <!DOCTYPE html>

    
 <html>
    
  
    
 <head>
    
     <meta charset="UTF-8">
    
     <title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title>
    
 </head>
    
  
    
 <body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0">
    
     <table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
    
     <tr>
    
         <td>(本邮件是程序自动下发的,请勿回复!)</td>
    
     </tr>
    
     <tr>
    
         <td>
    
             <h2>
    
                 <font color="#0000FF">构建结果 - ${BUILD_STATUS}</font>
    
             </h2>
    
         </td>
    
     </tr>
    
     <tr>
    
         <td><br />
    
             <b>
    
                 <font color="#0B610B">构建信息</font>
    
             </b>
    
             <hr size="2" width="100%" align="center" />
    
         </td>
    
     </tr>
    
     <tr>
    
         <td>
    
             <ul>
    
                 <li>项目名称&nbsp;:&nbsp;${PROJECT_NAME}</li>
    
                 <li>构建编号&nbsp;:&nbsp;第${BUILD_NUMBER}次构建</li>
    
                 <li>触发原因:&nbsp;${CAUSE}</li>
    
                 <li>构建日志:&nbsp;<a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
    
                 <li>构建&nbsp;&nbsp;Url&nbsp;:&nbsp;<a href="${BUILD_URL}">${BUILD_URL}</a></li>
    
                 <li>工作目录&nbsp;:&nbsp;<a href="${PROJECT_URL}ws">${PROJECT_URL}ws</a></li>
    
                 <li>项目&nbsp;&nbsp;Url&nbsp;:&nbsp;<a href="${PROJECT_URL}">${PROJECT_URL}</a></li>
    
             </ul>
    
         </td>
    
     </tr>
    
     <tr>
    
         <td><b>
    
                 <font color="#0B610B">Changes Since Last
    
                     Successful Build:</font>
    
             </b>
    
             <hr size="2" width="100%" align="center" />
    
         </td>
    
     </tr>
    
     <tr>
    
         <td>
    
             <ul>
    
                 <li>历史变更记录 : <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></li>
    
             </ul>
    
             ${CHANGES_SINCE_LAST_SUCCESS,reverse=true, format="Changes for Build #%n:<br />%c<br />",showPaths=true,changesFormat="
    
             <pre>[%a]<br/>%m</pre>
    
             ",pathFormat="&nbsp;&nbsp;&nbsp;&nbsp;%p"}
    
         </td>
    
     </tr>
    
     <tr>
    
         <td><b>Failed Test Results</b>
    
             <hr size="2" width="100%" align="center" />
    
         </td>
    
     </tr>
    
     <tr>
    
         <td>
    
             <pre style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">$FAILED_TESTS</pre>
    
             <br /></td>
    
     </tr>
    
     <tr>
    
         <td><b>
    
                 <font color="#0B610B">构建日志 (最后 100行):</font>
    
             </b>
    
             <hr size="2" width="100%" align="center" />
    
         </td>
    
     </tr>
    
     <tr>
    
         <td><textarea cols="80" rows="30" readonly="readonly" style="font-family: Courier New">${BUILD_LOG, maxLines=100}</textarea>
    
         </td>
    
     </tr>
    
     </table>
    
 </body>
    
  
    
 </html>
    
    
    
    
    AI助手

5. 编写脚本发送邮件

src/main/resources/jenkinsfile:

复制代码
 pipeline {

    
     agent any
    
  
    
     stages {
    
     ...
    
     }
    
  
    
     post {
    
       always {
    
 	   //1. 邮件内容指定 email.html 模板
    
     emailext  body: '${FILE,path="src/main/resources/email.html"}',
    
 			//2. 这个是邮件的标题
    
 			subject: '构建通知:${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS} !',
    
 			//3. 这个是邮件的接收者,多个邮件用 , 分割
    
 			to: 'xxb@163.com,xx010@qq.com'
    
       }
    
     }
    
 }
    
    
    
    
    AI助手

修改后记得上传到 gitlab 上。

构建后两个邮件收到的信息如下:

2、企业微信通知

需要安装插件Qy Wechat Notification,唯一不足当前插件不支持自定义文本。

复制代码
 post {

    
     always {
    
     qyWechatNotification mentionedId: '', mentionedMobile: '', webhookUrl: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxxxxx'
    
     }
    
 }
    
    
    
    
    AI助手

3、Http请求通知

需要安装插件HTTP Request,与第三方系统集成的常用方式。

复制代码
 post {

    
     always {
    
     httpRequest requestBody: '{\'build\':\'${env.BUILD_ID}\'}', responseHandle: 'NONE', url: 'http://192.168.1.100:8080/noti'
    
     }
    
 }
    
    
    
    
    AI助手

十、Jenkins 流水线构建插件

1、JenkinsFile添加环境变量和构建工具

1. 利用环境变量支持构建工具

平时,开发人员在搭建开发环境时做的就是:首先在机器上安装好构建工具,然后将这个构建工具所在目录加入PATH环境变量中。

如果想让Jenkins支持更多的构建工具,也是同样的做法:在Jenkins agent上安装构建工具,并记录下它的可执行命令的目录,然后在需要使用此命令的Jenkins pipeline的PATH环境变量中加入该可执行命令的目录。

示例如下:

复制代码
 pipeline {

    
     agent any 
    
     environment {
    
     PATH="/usr/lib/customtool/bin:$PATH"
    
     }
    
     stages {
    
     stage('build'){
    
         steps {
    
             sh "customtool build"y
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

还可以有另一种写法:

复制代码
 pipeline {

    
     agent any 
    
     environment {
    
     CUSTOM_TOOL_HOME = "/usr/lib/customtool/bin"
    
     }
    
     stages {
    
     stage('build'){
    
         steps {
    
             sh "${CUSTOM_TOOL_HOME}/customtool build"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

2. 利用tools作用域安装Maven

Jenkins pipeline的tools指令默认就支持Maven。所以,使用Maven只需要两步。

(1)进入Manage Jenkins→Global Tool Configuration→Maven页。

如果Name的值为mvn-3.5.4,接下来会用到这个值。“Install from Apache”下的Version可以选择Maven版本。

(2)在Jenkinsfile中指定Maven版本,并使用mvn命令。

复制代码
 pipeline {

    
     agent any 
    
     tools {
    
     maven 'mvn-3.5.4'
    
     }
    
     stages {
    
     stage('Example'){
    
         steps {
    
             sh 'mvn clean test install'
    
         }
    
     }
    
     }
    
 }
    
  
    
    
    
    
    AI助手

这样,当执行到tools指令时,Jenkins会自动下载并安装Maven。将mvn命令加入环境变量中,可以使我们在pipeline中直接执行mvn命令。

Maven默认使用的是其官方仓库,国内下载速度很慢。所以,我们通常会使用国内的Maven镜像仓库。这时就需要修改 Maven 的配置文件 settings.xml。settings.xml 文件的默认路径为${M2 HOME}/conf/settings.xml。但是,我们是不可能登录上Jenkins的机器,然后手动修改这个文件的。

Config File Provider插件(https://plugins.jenkins.io/config-file-provider)能很好地解决这个问题。只需要在Jenkins的界面上填入settings.xml的内容,然后在pipeline中指定settings.xml就可以了。也就是说,对于不同的pipeline,可以使用不同的settings.xml。

具体实现方法如下:

  1. 安装Config File Provider插件。
  2. 进入Manage Jenkins页面,就可以看到多出一个“Managed files”菜单。
  3. 单击“Managed files”进入,在左侧菜单栏中选择“Add a new Config”,就会看到该插件支持很多种配置文件的格式及方式。
  4. 选择“Global Maven settings.xml”选项。因为我们的设置是全局的。填写“ID”字段,Jenkins pipeline会引用此变量名。假如使用的ID为maven-global-settings。
  5. 单击“Submit”按钮提交后,就看到编辑页了。将自定义的Maven settings.xml的内容粘贴到“Content”字段中,单击“Submit”按钮即添加完成。

在Jenkins pipeline中使用的方法如下:

复制代码
 configFileProvider([configFile(fileId:'maven-global-settings', variable:'MAVEN_GLOBALENV')]){

    
     sh "mvn -s $MAVEN_GLOBAL_ENV clean install"
    
 }
    
    
    
    
    AI助手

3. Go语言环境搭建

Jenkins支持Golang的构建,只需要以下几步。

  1. 安装Go插件(https://plugins.jenkins.io/golang)。
  2. 进入Manage Jenkins→Global Tool Configuration→Go页。
  3. 在pipeline中加入tools部分。此时,在环境变量中会增加一个GOROOT变量。
复制代码
        1. tools {

    
        2.     go 'go1.10'
    
        3. }
    
    AI助手
  1. 设置GOPATH。了解Go语言开发都会知道,编译时需要设置GOPATH环境变量。直接在environment指令中添加就可以了。

完整代码如下:

复制代码
 pipeline {

    
     agent any 
    
     environment {
    
     GOPATH="${env.WORKSPACE}/"
    
     }
    
     tools {
    
     go 'go1.10'
    
     }
    
     stages {
    
     stage('build'){
    
         steps {
    
             sh "go build"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

4. Python环境搭建

Python环境很容易产生Python版本冲突、第三方库冲突等问题。所以,Python开发通常会进行工程级别的环境隔离,也就是每个Python工程使用一个Python环境。

在Jenkins环境下,我们使用Pyenv Pipeline插件(https://plugins.jenkins.io/pyenv-pipeline)可以轻松地实现。

首先,准备Python基础环境。

(1)在Jenkins机器上安装python、pip、virtualenv。

  • pip:Python的包管理工具。
  • virtualenv:Python中的虚拟环境管理工具。

(2)安装Pyenv Pipeline插件。

然后,在pipeline中使用Pyenv Pipeline插件提供的withPythonEnv方法。

复制代码
 withPythonEnv('/usr/bin/python') {

    
     sh 'python --version'
    
 }
    
    
    
    
    AI助手

withPythonEnv方法会根据第一个参数——可执行python路径——在当前工作空间下创建一个virtualenv环境。

withPythonEnv方法的第二个参数是一个闭包。闭包内的代码就执行在新建的virtualenv环境下。

5. 实现多版本编译

在实际工作中,有时需要对同一份源码使用多个版本的编译器进行编译。tools指令除了支持pipeline作用域,还支持stage作用域。所以,我们可以在同一个pipeline中实现多版本编译。

代码如下:

复制代码
 pipeline {

    
     agent any 
    
     stages{
    
     stage("build with jdk-10.0.2"){
    
         tools{
    
             jdk "jdk-10.0.2"
    
         steps{
    
             sh "printenv"
    
         }
    
     }
    
     stage("build with jdk-9.0.4") {
    
         tools {
    
             jdk "jdk-9.0.4"
    
         }
    
         steps{
    
             sh "printenv"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

在打印出来的日志中,会发现每个stage下的JAVA_HOME变量的值都不一样。

2、SSH 相关插件

支持SSH文件上传的插件目前主要有Publish Over SSH、SSH Pipeline Steps。

1. Publish Over SSH

需要先在全局配置中配置远程服务器信息:

Jenkinsfile中脚本代码:

复制代码
 stage('部署和启动') {

    
       sshPublisher(
    
             publishers: [
    
                     sshPublisherDesc(
    
                             configName: "192.168.28.129_test1",
    
                             transfers: [
    
                                     sshTransfer(cleanRemote: false,
    
                                             excludes: '',
    
                                             execCommand: """
    
                                                     cd ${deploy_dir}         // 远程执行的shell
    
                                             """,
    
                                             execTimeout: 120000,
    
                                             flatten: false,
    
                                             makeEmptyDirs: false,
    
                                             noDefaultExcludes: false,
    
                                             patternSeparator: '[, ]+',
    
                                             remoteDirectory: "upload",        // 远程目录
    
                                             remoteDirectorySDF: false,
    
                                             removePrefix: "penneo/",          // 删掉的去缀
    
                                             sourceFiles: "penneo/test.zip")   // 上传的文件
    
                             ],
    
                             usePromotionTimestamp: false,
    
                             useWorkspaceInPromotion: false,
    
                             verbose: true)
    
             ])
    
     }
    
    
    
    
    AI助手

2. SSH Pipeline Steps

复制代码
 node {

    
     def remote = [:]
    
     remote.name = ‘test’
    
     remote.host = ‘192.168.28.129’
    
     remote.user = ‘penngo’
    
     remote.password = ‘123456’
    
     remote.allowAnyHosts = true
    
     stage(‘Remote SSH’) {
    
     writeFile file: ‘abc.sh’, text: ‘ls’
    
     sshCommand remote: remote, command: ‘ls -al’
    
     sshPut remote: remote, from: ‘abc.sh’, into: ‘.’
    
     sshGet remote: remote, from: ‘abc.sh’, into: ‘bac.sh’, override: true
    
     sshScript remote: remote, script: ‘abc.sh’
    
     sshRemove remote: remote, path: ‘abc.sh’
    
     }
    
 }
    
    
    
    
    AI助手

也可以结合withCredentials使用:

复制代码
     stages {

    
     stage('部署') {
    
         steps {
    
                 script{
    
                     def ip = '192.168.28.132'
    
                     withCredentials([sshUserPrivateKey(credentialsId: "${ip}_pk", keyFileVariable: 'identity', usernameVariable: 'username')]) {
    
                         def remote = [:]
    
  
    
                         remote.name = ip
    
                         remote.host = ip
    
                         remote.allowAnyHosts=true
    
                         remote.user = username
    
                         remote.identityFile = identity
    
                         sshCommand remote: remote, command: """
    
                             pwd
    
                             ls -al
    
                         """
    
                         // 把远程主机文件下载到本地工作区
    
                         sshGet remote: remote, from: 'build_id.txt', into: 'build2_id.txt', override: true
    
                         // 本地工作区文件上传到远程主机目录
    
                         sshPut remote: remote, from: 'JApiTest.zip', into: '/data/upload/'
    
                     }
    
  
    
  
    
                     withCredentials([usernamePassword(credentialsId: "${ip}", passwordVariable: 'password', usernameVariable: 'username')]) {
    
                         def remote = [:]
    
                         remote.name = ip
    
                         remote.host = ip
    
                         remote.user = username
    
                         remote.password = password
    
                         remote.allowAnyHosts=true
    
                         sshCommand remote: remote, command: """
    
                             cd /usr/local
    
                             pwd
    
                             ls -al
    
                         """
    
                         sshGet remote: remote, from: 'build_id.txt', into: 'build2_id.txt', override: true
    
                         sshPut remote: remote, from: 'JApiTest.zip', into: '/data/upload/'
    
 //                            sshScript remote: remote, script: 'server.sh'
    
                         sshRemove remote: remote, path: '/root/ttt_test'
    
                     }
    
                 }
    
         }
    
     }
    
     }
    
    
    
    
    AI助手

3、git plugin 插件

作用:使用 git 命令 checkout 出项目的代码,在 pipeline 代码中,很常见在不同 stage 中使用不同 git 仓库地址的代码,所以 git SCM 操作,可以写在不同 stage 中。

复制代码
 import hudson.model.*;

    
  
    
 println env.JOB_NAME
    
 println env.BUILD_NUMBER
    
  
    
 pipeline{
    
     agent any
    
     stages{
    
     stage("test") {
    
         steps{
    
             script {
    
                 println "test"
    
             }
    
         }
    
     }
    
     stage("git checkout") {
    
         steps{
    
             script {
    
                 checkout([$class: 'GitSCM',
    
                 branches: [[name: '*/master']],
    
                 doGenerateSubmoduleConfigurations: false,
    
                 userRemoteConfigs: [[credentialsId: 'gtihub', url:
    
                 'https://github.com/kongsh8778/pipelinetest']]])
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

4、publish html report 插件

作用:将测试完成后生成的 html 格式的测试报告展示在 Jenkins 中,无需再切换到目录用浏览器打开。

复制代码
 import hudson.model.*;

    
  
    
 pipeline{
    
     agent any
    
     parameters {
    
     string(name: 'BROWSER_TYPE', defaultValue: 'chrome', description: 'Type a
    
     browser type, should be chrome/firefox')
    
     string(name: 'TEST_SERVER_URL', defaultValue: '', description: 'Type the
    
     test server url')
    
     string(name: 'NODE', defaultValue: 'win-anthony-demo', description:
    
     'Please choose a windows node to execute this job.')
    
     }
    
     stages{
    
     stage("test"){
    
         steps{
    
             script{
    
                 browser_type = BROWSER_TYPE?.trim()
    
                 test_url = TEST_SERVER_URL?.trim()
    
                 win_node = NODE?.trim()
    
                 echo 'test'
    
             }
    
         }
    
     }
    
     }
    
     post {
    
     always{
    
         script{
    
             publishHTML (target: [
    
                 allowMissing: false,
    
                 alwaysLinkToLastBuild: false,
    
                 keepAll: true,
    
                 reportDir: '../../jobs/test_pipeline_demo/report',
    
                 reportFiles: '接口测试报告.html',
    
                 reportName: "接口测试报告"
    
             ])
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

插件参数说明:

  • reportDir:项目中保存 html 文件的地方,这里写的是一个相对路径写法,相对于当前工作目录的路径,不写的话默认是项目根目录。
  • reportFiles:需要展示的 html 文件名,也可以同时写多个 html 文件,逗号隔开。
  • reportName:这个参数指定的字符串会在 Jenkins 构建 Job 页面显示的菜单名称,后面会看到这个名称,这个名称可以随意修改。

构建结果:

手动构建后,在项目首页左侧的导航栏可以看到上一步 reportName 值指定的菜单名:

单击接口测试报告,可以看到如下展示结果:

5、凭证管理插件

1. 优雅的使用凭证

在Jenkinsfile或部署脚本中使用明文密码会造成安全隐患,频繁出现明文密码被上传到GitHub上是一种十分不安全的行为。

凭证(cridential)是Jenkins进行受限操作时的凭据。比如使用SSH登录远程机器时,用户名和密码或SSH key就是凭证。而这些凭证不可能以明文写在Jenkinsfile中。Jenkins凭证管理指的就是对这些凭证进行管理。

为了最大限度地提高安全性,在Jenkins master节点上对凭证进行加密存储(通过Jenkins实例ID加密),只有通过它们的凭证ID才能在pipeline中使用,并且限制了将证书从一个Jenkins实例复制到另一个Jenkins实例的能力。

也因为所有的凭证都被存储在Jenkins master上,所以在Jenkins master上最好不要执行任务,以免被pipeline非法读取出来。

那么在哪里执行pipeline呢?应该分配到Jenkins agent上执行。

声明式pipeline提供了credentials helper方法(只能在environment指令中使用)来简化凭证的使用。

以下是使用方法:

  • Secret text
复制代码
 environment {

    
     AWS_ACCESS_KEY_ID = credentials('aws-secret-key-id')
    
     AWS_SECRET_ACCESS_KEY = credentials('aws-secret-access-key')
    
 }
    
    
    
    
    AI助手

AWS-SECRET-KEY-ID和AWS-SECRET-ACCESS-KEY是我们预先定义的凭证ID。creden-tials方法将凭证的值赋给变量后,我们就可以像使用普通环境变量一样使用它们了,如:echo "${AWS ACCESS KEY ID}"。

  • Username with password
复制代码
 environment {

    
     BITBUCKET_CREDS= credentials('jenkins-bitbucket-creds')
    
 }
    
    
    
    
    AI助手

与 Secret text 不同的是,我们需要通过 BITBUCKET CREDS USR 拿到用户名的值,通过BITBUCKET CREDS PSW拿到密码的值。而变量BITBUCKET CREDS的值则是一个字符串,格式为:<用户名>:<密码>。

  • Secret file
复制代码
 environment {

    
     KNOWN_HOSTS = credentials('known_hosts')
    
 }
    
    
    
    
    AI助手

通过credentials helper方法,我们可以像使用环境变量一样使用凭证。

但遗憾的是,credentials helper方法只支持Secret text、Username with password、Secret file三种凭证。

2. HashiCorp Vault插件

如果觉得Jenkins的凭证管理功能太弱,无法满足你的需求,则可以考虑使用HashiCorp Vault插件。

HashiCorp Vault是一款对敏感信息进行存储,并进行访问控制的工具。敏感信息指的是密码、token、密钥等。它不仅可以存储敏感信息,还具有滚动更新、审计等功能。

安装HashiCorp Vault插件(https://plugins.jenkins.io/hashicorp-vault-plugin)。

HashiCorp Vault插件并没有提供pipeline步骤,提供此步骤的是Hashicorp Vault Pipeline插件(https://github.com/jenkinsci/hashicorp-vault-pipeline-plugin)。但是它依赖的是2.138.1或以上的Jenkins版本。

如果你的Jenkins版本低于2.138.1,但是又想用Hashicorp Vault Pipeline插件,怎么办呢?

可以将该插件的源码下载到本地,将pom.xml中的jenkins.version值从2.138.1修改成你的Jenkins版本,然后运行mvn clean package进行编译打包。如果没有报错,则接着找到target/hashicorp-vault-pipeline.hpi进行手动安装即可。

当前使用的Jenkins版本为2.121.3,经测试没有问题。

首先我们使用 vault 命令向 vault 服务写入私密数据以方便测试:vault write secret/hello value=world。

接着在pipeline中读取,示例代码如下:

复制代码
 pipeline {

    
     agent any
    
     environment {
    
     SECRET = vault path: 'secret/hello', key:' value'
    
     }
    
     stages {
    
     stage("read vault key") {
    
         steps {
    
             script{
    
                 def x = vault path: 'secret/hello', key: 'value'
    
                 echo "${x}"
    
                 echo "${SECRET}"
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

我们可以在environment和steps中使用vault步骤。推荐在environment中使用。

vault步骤的参数如下:

  • path,存储键值对的路径。
  • key,存储内容的键。
  • vaultUrl(可选),vault服务地址。
  • credentialsId(可选),vault服务认证的凭证。

如果不填vaultUrl与credentialsId参数,则使用系统级别的配置。

3. 在Jenkins日志中隐藏敏感信息

如果使用的是credentials helper方法或者withCredentials步骤为变量赋值的,那么这个变量的值是不会被明文打印到Jenkins日志中的。

除非使用以下方法:

复制代码
 steps {

    
     script{
    
     def hack = 'hack it'
    
     withCredentials([string(credentialsId: 'abc', variable: 'secretText')]) {
    
         echo "${secretText}" // 打印 **** 
    
         hack = "${secretText}"
    
     }
    
     echo "${hack}" //打印出明文:test
    
     }
    
 }
    
    
    
    
    AI助手

在没有使用credential的场景下,我们又该如何在日志中隐藏变量呢?

可以使用Masked Pass-word插件(https://plugins.jenkins.io/mask-passwords)。通过该插件提供的包装器,可以隐藏我们指定的敏感信息。

示例代码如下:

复制代码
 pipeline {

    
     agent any 
    
     environment {
    
     SECRET1 = "secret1"
    
     SECRET2 = "secret2"
    
     NOT_SECRET= "no secret"
    
     }
    
     stages {
    
     stage("read vault key") {
    
         steps {
    
             wrap([$class: 'MaskPasswordsBuildWrapper', varPasswordPairs: [
    
                 [password: env['SECRET1'], var: 's1'], 
    
                 [password: env['SECRET2'], var: 's2']])
    
             ){
    
             echo "被隐藏的密文:${SECRET1} 和 ${SECRET2}"
    
             echo "secret1" //这也会被隐藏,打印成********
    
             echo "明文打印: ${NOT_SECRET}"
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

初次使用 Masked Password 插件很容易以为是使用 s1 和 s2 作为变量的,如 echo"被隐藏的密文:{s1} 和 {s2}"。实际上,var参数只是用于方便在自由风格的Jenkins项目中区分不同的需要隐藏的密文。在pipeline中使用,它就没有存在的意义了。但是即使这样也不能省略它,必须传一个值。password参数传的是真正要隐藏的密文。

那么,为什么echo "secret1"这条语句中并没有使用预定义的变量,secret1也会被隐藏呢?这是由Masked Password插件的实现方式决定的。

Jenkins 提供了 ConsoleLogFilter 接口,可以在日志打印阶段实现我们自己的业务逻辑。Masked Password 插件实现了 ConsoleLogFilter 接口,然后利用正则表达式将匹配到的文本replaceAll成********。

MaskPasswordsBuildWrapper包装器除了支持varPasswordPairs参数,还支持varMask Regexes参数,使用自定义的正则表达式匹配需要隐藏的文本。写法如下:

复制代码
 steps {

    
     wrap([$class: 'MaskPasswordsBuildWrapper',
    
     varMaskRegexes: [[regex: 'abc-.*']]
    
     ]
    
     ) {
    
     echo "abc-xxxx"
    
     }
    
 }
    
    
    
    
    AI助手

通过Masked Password插件还可以设置全局级别的密文隐藏,在Manage Jenkins→Configure System页中可以找到。

6、BlueOcean

BlueOcean 是 Jenkins 团队从用户体验角度出发,专为 Jenkins Pipeline 重新设计的一套 UI 界面,仍然兼容以前的 fressstyle 类型的 job,BlueOcean 具有以下的一些特性:

  • 连续交付(CD)Pipeline 的复杂可视化,允许快速直观的了解 Pipeline 的状态
  • 可以通过 Pipeline 编辑器直观的创建 Pipeline
  • 需要干预或者出现问题时快速定位,BlueOcean 显示了 Pipeline 需要注意的地方,便于异常处理和提高生产力
  • 用于分支和拉取请求的本地集成可以在 GitHub 或者 Bitbucket 中与其他人进行代码协作时最大限度提高开发人员的生产力。

BlueOcean 可以安装在现有的 Jenkins 环境中,也可以使用 Docker 镜像的方式直接运行,我们这里直接在现有的 Jenkins 环境中安装 BlueOcean 插件:登录 Jenkins Web UI -> 点击左侧的 Manage Jenkins -> Manage Plugins -> Available -> 搜索查找 BlueOcean -> 点击下载安装并重启

创建Pipeline:

获取Token的步骤:

然后获取Token:

创建完成如下所示:

十一、Jenkins 流水线制品库集成

1、制品简介

制品是软件开发过程中产生的多种有形副产品之一。另外,直译artifact,是人工制品的意思。所以,广义的制品还包括用例、UML图、设计文档等。

而狭义的制品就可以简单地理解为二进制包。虽然有些代码是不需要编译就可以执行的,但是我们还是习惯于将这些可执行文件的集合称为二进制包。

最简单的制品管理仓库就是将制品统一放在一个系统目录结构下。但是很少有人这样做,更多的做法是使用现成的制品库。

制品管理涉及两件事情:一是如何将制品放到制品库中;二是如何从制品库中取出制品。

由于每种制品的使用方式不一样,目前现成的制品库有:Nexus(https://www.sonatype.com/nexus-repository-oss)、Artifactory。

从手工打包到自动化打包,再将打好的包放到制品库中。这看似简单,但是要在团队中从无到有地落地其实是一个很漫长的过程,特别是对于存在很多遗留项目的团队。每个团队都应该按照自己当前情况进行调整,有时统一的解决方案不一定适合你。

假如已经将部分项目的编译和单元测试放到Jenkins上执行,然而并没有人力及能力搭建Nexus。但是又期望能将自动打包好的JAR包放到各个环境中使用,以马上从持续集成中获益,怎么办?

这时,archiveArtifacts步骤(https://jenkins.io/doc/pipeline/steps/core/#archiveartifacts-arch-ive-the-artifacts)就派上用场了。它能对制品进行归档,然后你就可以从Jenkins页面上下载制品了。

完整的Jenkinsfile内容如下:

复制代码
 tools {

    
     maven 'mvn-3.5.4'
    
     }
    
     stages {
    
     stage('Build'){
    
         steps {
    
             sh "mvn clean spring-boot:repackage"
    
         }
    
     }
    
     }
    
     post {
    
     always{
    
         archiveArtifacts artifacts: 'target/**/*.jar', fingerprint: true 
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

archiveArtifacts的参数的用法。

  • artifacts(必填):字符串类型,需要归档的文件路径,使用的是Ant风格路径表达式。
  • fingerprint(可选):布尔类型,是否对归档的文件进行签名。
  • excludes(可选):字符串类型,需要排除的文件路径,使用的也是Ant风格路径表达式。
  • caseSensitive(可选):布尔类型,对路径大小写是否敏感。
  • onlyIfSuccessful(可选):布尔类型,只在构建成功时进行归档。

值得一提的是,archiveArtifacts步骤并不只用于归档JAR包,事实上,它能归档所有类型的制品。
团队初期可以考虑使用这种方式管理简单的制品。

仓库命名规范:

  • 业务/项目-环境类型例如:demo-dev

制品命名规范:

  • 应用名称-版本号-构建ID.type
  • 例如: demo-myapp-service-1.jar

制品目录规范:

  • 业务/项目
  • 应用名称
  • 版本号
  • 制品

2、Nexus 制品库

1. 管理Java制品库

目前Java栈的构建工具以Maven及Gradle为主,且Maven的用户最广泛。接下来,我们使用Maven作为主要工具来讲解制品管理。

当Nexus搭建好后,就可以使用Maven Deploy插件上传JAR或WAR包到Nexus中了。Deploy插件是Apache Maven团队提供的官方插件,能将JAR包及POM文件发布到Nexus中。

在POM文件中这样定义:

复制代码
 <plugins>

    
     <plugin>
    
     <groupId>org.apache.maven.plugins</groupId>
    
     <artifactId>maven-deploy-plugin</artifactId>
    
     <version>2.8.2</version>
    
     </plugin>
    
 </plugins>
    
    
    
    
    AI助手

如果不需要自定义Deploy插件配置,则不需要在POM文件中定义。

使用Deploy插件发布需要以下几个步骤。

(1)配置发布地址。在Maven项目的POM文件中加入:

复制代码
 <distributionManagement>

    
     <snapshotRepository>
    
     <id>nexus-snapshot</id>
    
     <name>my nexus snapshot</name>
    
     <url>http://<你的Nexus地址>/repository/maven-snapshots</url>
    
     </snapshotRepository>
    
     <repository>
    
     <id>nexus-release</id>
    
     <name>my nexus release</name>
    
     <url>http://<你的Nexus地址>/repository/maven-releases</url>
    
     </repository>
    
 </distributionManagement>
    
    
    
    
    AI助手

完成此步骤后,我们就可以通过执行mvn clean deploy进行发布了。Deploy插件会根据Maven项目中定义的version值决定是使用nexus-snapshot仓库还是nexus-release仓库。当version值是以-SNAPSHOT后缀结尾时,则发布到nexus-snapshot仓库。

(2)配置访问Nexus的用户名和密码。在Nexus中,我们配置了只有授权的用户名和密码才能发布制品。

这时需要在Maven的settings.xml中加入配置:

复制代码
 <servers>

    
     <server>
    
     <id>nexus-snapshot</id>
    
     <username>admin</username>
    
     <password>admin123</password>
    
     </server>
    
     <server>
    
     <id>nexus-release</id>
    
     <username>admin</username>
    
     <password>admin123</password>
    
     </server>
    
 </servers>
    
    
    
    
    AI助手

2. 使用Nexus插件发布制品

除了可以通过Maven发布JAR包,还可以使用Nexus Platform(https://plugins.jenkins.io/nexus-jenkins-plugin)来插件实现。

最新版本的Nexus Platform已经同时支持Nexus 2.x和Nexus 3.x,只是它的文档更新不及时,大家都不知道它支持3.x版本了。

在安装好Nexus Platform插件后,根据以下步骤来使用。

(1)进入Manage Jenkins→Configure System→Sonatype Nexus页,设置Nexus 3.x的服务器地址。

需要注意的是:

  • 在“Credentials”选项处,增加了一个具有发布制品到Nexus中的权限的用户名和密码凭证。
  • Server ID字段的值,在Jenkinsfile中会引用。

设置完成后,单击“Test connection”按钮测试设置是否正确。

(2)在Jenkinsfile中加入nexusPublisher步骤。

复制代码
 stage('Build') {

    
     steps {
    
     sh "mvn clean test package"
    
     nexusPublisher(
    
         nexusInstanceId: 'nexus3',
    
         nexusRepositoryId: 'maven-releases', 
    
         packages: [
    
         [
    
         $class: 'MavenPackage', 
    
         mavenAssetList: [
    
             [classifier:'',
    
                 extension:'',
    
                 filePath:'./target/server-1.0-SNAPSHOT.jar'
    
             ]
    
         ],//end of mavenAssetList 
    
         mavenCoordinate: [
    
         artifactId:'server', 
    
         groupId: 'codes.showme', 
    
         packaging:'jar', version:'1.0'
    
         ]
    
      ] // end of packages 
    
     ])
    
 }
    
    
    
    
    AI助手

nexusPublisher的参数:

  • nexusInstanceId:在Jenkins中配置Nexus 3.x时的Server ID。
  • nexusRepositoryId:发布到Nexus服务器的哪个仓库。
  • mavenCoordinate:Maven包的坐标,packaging值与Maven中的packaging值一致,可以是jar、war、pom、hpi等。
  • mavenAssetList:要发布的文件,如果是pom.xml,则extension必须填“xml”。

在实际工作中,并不常使用此插件。原因如下:

  • 每个Maven项目都可能不同,必须为每个Maven项目写nexusPublisher方法。
  • 对于多模块的Maven项目,nexusPublisher的参数写起来十分啰唆。

但是介绍这个插件还是有必要的,一是大家可以根据实际情况进行选择;二是可以了解Jenk-ins与Nexus的集成程度。

3. 使用Nexus管理Docker镜像

Jenkins机器上已经安装了Docker CE。检查在Jenkins上能否运行Docker的方法是:在Jenkinsfile中加入sh "docker ps"语句,如果没有报错,就说明可以运行Docker。

当私有仓库创建好后,我们就可以构建Docker镜像并发布到仓库中了。

假设Dockerfile与Jenkinsfile在同一个目录下,我们看一下Jenkinsfile的内容。

复制代码
 pipeline {

    
     agent any 
    
     environment {
    
     registry = "http://192.168.0.101:8595"
    
     registryCredential = 'dockernexus'
    
     }
    
     stages {
    
     stage('Build') {
    
         steps {
    
             withDockerRegistry([
    
                 credentialsId: "${registryCredential}", 
    
                 url: "${registry}"]) {
    
                 sh "docker build . -t ${registry}/hello:v2"
    
                 sh "docker push ${registry}/hello:v2"
    
             }
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

withDockerRegistry步骤做的事情实际上就是先执行命令:docker login-u admin-p********http://192.168.0.101:8595。

其间,所生成的config.json文件会存储在工作空间中。然后再执行闭包内的命令。

由于是私有的非安全(HTTP)的仓库,所以需要配置Docker的daemon.json。

复制代码
 {

    
     "insecure-registries": ["<私有仓库的地址>"],
    
     "registry-mirrors": ["https://registry.docker-cn.com"]
    
 }
    
    
    
    
    AI助手

同时,为加速基础镜像的下载,设置了国内Docker镜像。

4. 从其他pipeline中拷贝制品

在某些场景下,我们需要从另一个pipeline中拷贝制品。

Copy Artifact插件(https://plugins.jenkins.io/copyartifact)可以帮助我们实现,具体代码如下:

复制代码
 steps {

    
     copyArtifacts(
    
     projectName: "core",
    
     selector: lastSuccessful(true)
    
     )
    
 }
    
    
    
    
    AI助手

从core项目中拿到最后一次构建成功的制品。

接下来,我们详细介绍copyArtifacts步骤的参数。

  • projectname:字符串类型,Jenkins job或pipeline名称。
  • selector:BuildSelector类型,从另一个pipeline中拷贝制品的选择器,默认拷贝最后一个制品。
  • parameters:字符串类型,使用逗号分隔的键值对字符串(name1=value1,name2=value2),用于过滤从哪些构建中拷贝制品。
  • filter:字符串类型,Ant风格路径表达式,用于过滤需要拷贝的文件。
  • excludes:字符串类型,Ant风格路径表达式,用于排除不需要拷贝的文件。
  • target:字符串类型,拷贝制品的目标路径,默认为当前pipeline的工作目录。
  • optional:布尔类型,如果为true,则拷贝失败,但不影响本次构建结果。
  • fingerprintArtifacts:布尔类型,是否对制品进行签名,默认值为true。
  • resultVariableSuffix:上例中,无法得知我们到底拿的是core项目的哪次构建的制品。Copy Artifact 插件的设计是将其构建次数放到一个环境变量中。这个环境变量名就是在COPYARTIFACT BUILD NUMBER 后拼上resultVariableSuffix,比如resultVariableSuf fix值为corejob,那么就在pipeline中通过变量COPYARTIFACT BUILD NUMBER corejob拿到源pipeline的构建次数了。

除projectname参数是必填的外,其他参数都是可选的。

下面介绍几种常用的获取选择器的方法。

  • lastSuccessful:最后一次构建成功的制品。方法签名为lastSuccessful(boolean stable)。stable为true表示只取构建成功的制品,为false表示只要构建结果比UNSTABLE好就行。
  • specific:指定某一次构建的制品。方法签名为specific(String buildNumber)。buildNum ber表示指定取第n次构建的制品。
  • lastCompleted:最后一次完成构建的制品,不论构建的最终状态如何。方法签名为lastCompleted()。
  • latestSavedBuild:最后一次被标记为keep forever的构建的制品。方法签名为latestSavedBu ild()。

3、Artifactory 制品库

安装Artifactory插件:

配置Artifactory仓库信息(仓库地址、用户认证信息):

构建制品:

artifactory.groovy:

复制代码
 package org.devops

    
  
    
  
    
 //Maven打包构建
    
 def MavenBuild(buildShell){
    
     def server = Artifactory.newServer url: "http://192.168.1.200:30082/artifactory"
    
     def rtMaven = Artifactory.newMavenBuild()
    
     def buildInfo
    
     server.connection.timeout = 300
    
     server.credentialsId = 'artifactory-admin-user' 
    
     //maven打包
    
     rtMaven.tool = 'M2' 
    
     buildInfo = Artifactory.newBuildInfo()
    
  
    
     String newBuildShell = "${buildShell}".toString()
    
     println(newBuildShell)
    
     rtMaven.run pom: 'pom.xml', goals: newBuildShell, buildInfo: buildInfo
    
     //上传build信息
    
     server.publishBuildInfo buildInfo
    
 }
    
  
    
  
    
  
    
 //上传制品
    
 def PushArtifact(){
    
     
    
     
    
     //重命名制品
    
     def jarName = sh returnStdout: true, script: "cd target;ls *.jar"
    
     jarName = jarName - "\n"
    
     def pom = readMavenPom file: 'pom.xml'
    
     env.pomVersion = "${pom.version}"
    
     env.serviceName = "${JOB_NAME}".split("_")[0]
    
     env.buildTag = "${BUILD_ID}"
    
     def newJarName = "${serviceName}-${pomVersion}-${buildTag}.jar"
    
     println("${jarName}  ------->>> ${newJarName}")
    
     sh " mv target/${jarName}  target/${newJarName}"
    
     
    
     //上传制品
    
     env.businessName = "${env.JOB_NAME}".split("-")[0]
    
     env.repoName = "${businessName}-${JOB_NAME.split("_")[-1].toLowerCase()}"
    
     println("本次制品将要上传到${repoName}仓库中!")   
    
     env.uploadDir = "${repoName}/${businessName}/${serviceName}/${pomVersion}"
    
     
    
     println('上传制品')
    
     rtUpload (
    
     serverId: "artifactory",
    
     spec:
    
         """{
    
         "files": [
    
             {
    
             "pattern": "target/${newJarName}",
    
             "target": "${uploadDir}/"
    
             }
    
         ]
    
         }"""
    
     )
    
 }
    
  
    
  
    
 def main(buildType,buildShell){
    
     if(buildType == "mvn"){
    
     MavenBuild(buildShell)
    
     }
    
 }
    
    
    
    
    AI助手

JenkinsFile:

复制代码
 #!groovy

    
  
    
 @Library('jenkinslibrary@master') _
    
  
    
 //func from shareibrary
    
 def artifactory = new org.devops.artifactory() 
    
  
    
 //pipeline
    
 pipeline{
    
     agent { node { label "build"}}
    
     
    
     
    
     stages{
    
  
    
     stage("CheckOut"){
    
         steps{
    
             script{
    
                
    
                 
    
                 println("${branchName}")
    
             
    
                 tools.PrintMes("获取代码","green")
    
                 checkout([$class: 'GitSCM', branches: [[name: "${branchName}"]], 
    
                                   doGenerateSubmoduleConfigurations: false, 
    
                                   extensions: [], 
    
                                   submoduleCfg: [], 
    
                                   userRemoteConfigs: [[credentialsId: 'gitlab-admin-user', url: "${srcUrl}"]]])
    
  
    
             }
    
         }
    
     }
    
     stage("Build"){
    
         steps{
    
             script{
    
             
    
                 tools.PrintMes("执行打包","green")
    
                 //build.Build(buildType,buildShell)
    
                 artifactory.main(buildType,buildShell)
    
                 artifactory.PushArtifact()
    
                 
    
                 //上传制品
    
                 //nexus.main("nexus")
    
                 
    
                 //发布制品
    
                 //sh " wget ${artifactUrl} && ls "
    
  
    
  
    
                 //deploy.SaltDeploy("${deployHosts}","test.ping")
    
                 //deploy.AnsibleDeploy("${deployHosts}","-m ping ")
    
             }
    
         }
    
    }
    
 }
    
    
    
    
    AI助手

构建结果:

十二、Jenkins 多分支流水线构建

1、Multibranch Pipeline模式项目

而在实际项目中,往往需要多分支同时进行开发。如果为每个分支都分别创建一个Jenkins项目,实在有些多余。幸好Jenkins支持多分支pipeline(Multibranch Pipeline)。

在创建此类项目时,就需要选择“Multibranch Pipeline”。

因为jenkins需要感知git项目分支,因此需要配置git项目地址。

直接选择默认选项,通过git项目Jenkinsfile脚本定义执行构建动作。

一个git项目分支基本是立项确定,变动很少,但仍会存在增删改情况,因此设置定时任务自动扫描分支。

到这里就配置完了。前面的两个模式都有构建触发器配置,多分支管道模式比较特殊,其构建触发器需要在Jenkinsfile内定义,且目前官方只支持三种触发模式:

  1. cron:定时拉取代码构建;
  2. pollSCM:定时拉取代码,存在新变更时构建;
  3. upsteam;

git项目新增Jenkinsfile文件:

  • 在develop分支新建文件Jenkinsfile;
  • 在master分支合并develop分支;

Jenkinsfile脚本内容如下:

复制代码
 pipeline {

    
     agent any
    
     stages {
    
     stage('Checkout') {
    
         steps {
    
             echo '开始拉取代码...'
    
             cleanWs()
    
             git branch: '$BRANCH_NAME', credentialsId: '1b458187-1203-4ef5-8f9a-97fe7576e4b1', url: 'http://gitea:3000/root/my-multibranch-pipeline.git'
    
         }
    
     }
    
     stage('Build') {
    
         steps {
    
             echo '开始构建代码...'
    
         }
    
     }
    
     stage('Archive') {
    
         steps {
    
             echo '开始打包文件...'
    
             archiveArtifacts '**/*'
    
         }
    
     }
    
     stage('Deploy') {
    
         steps {
    
             echo '开始部署文件...'
    
             sshPublisher(publishers: [sshPublisherDesc(configName: 'nginx', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: 'nginx -s reload', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '$BRANCH_NAME', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '**/*')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
    
         }
    
     }
    
     }
    
     triggers {
    
     pollSCM 'H/2 * * * *'
    
     }
    
 }
    
    
    
    
    AI助手

点击扫描按钮:

jenkins项目首页即可看到维护的分支信息:

测试:

在develop分支新增变更提交,大概两分钟后即可看到项目自动触发进行项目构建动作。

2、根据分支部署到不同的环境

git分支可以用于对代码进行物理隔离。对分支的管理有很多方法,比如主干开发,发布分支以及Gitflow法等。我们不讨论它们的好坏。

但不论使用哪种分支管理方法,都可能会涉及一个问题:如何根据不同的分支做不同的事情,比如根据不同的分支部署到不同的环境。类似这样的事情可以使用if-else来实现。

复制代码
 stage("deploy to test"){

    
     steps{
    
     script {
    
         if (env.GIT_BRANCH == 'master'){
    
             echo "deploy to test env" 
    
         }
    
     }
    
     }
    
 }
    
 stage("deploy to prod"){
    
     steps{
    
     script {
    
         if (env.GIT_BRANCH == 'release') {
    
             echo "deploy to prod"
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

但是这样的代码不够优雅,而且不是声明式的。使用when指令可以让pipeline看起来更优雅。

复制代码
 stage("deploy to test"){

    
     when {
    
     branch 'master'
    
     }
    
     steps {
    
     echo "deploy to test"
    
     }
    
 }
    
  
    
 stage("deploy to prod"){
    
     when {
    
     branch 'release'
    
     }
    
     steps{
    
     echo "deploy to prod"
    
     }
    
 }
    
    
    
    
    AI助手

when指令允许pipeline根据给定的条件,决定是否执行阶段内的步骤。when指令必须至少包含一个条件。when指令除了支持branch判断条件,还支持多种判断条件。

  • changelog:如果版本控制库的changelog符合正则表达式,则执行:
复制代码
 when {

    
     changelog '.*^\ [DEPENDENCY\ ] .+$'
    
 }
    
    
    
    
    AI助手
  • changeset:如果版本控制库的变更集合中包含一个或多个文件符合给定的Ant风格路径表达式,则执行:
复制代码
 when {

    
     changeset "**/*.js"
    
 }
    
    
    
    
    AI助手
  • environment:如果环境变量的值与给定的值相同,则执行:
复制代码
 when {

    
     environment name: 'DEPLOY_TO', value: 'production'
    
 }
    
    
    
    
    AI助手
  • equals:如果期望值与给定的值相同,则执行:
复制代码
 when {

    
 equals expected: 2, actual: currentBuild.number 
    
 }
    
    
    
    
    AI助手
  • expression:如果Groovy表达式返回的是true,则执行:
复制代码
 when {

    
     expression {
    
     return env.BRANCH_NAME != 'master';
    
     }
    
 }
    
    
    
    
    AI助手

当表达式返回的是字符串时,它必须转换成布尔类型或null;否则,所有的字符串都被当作true处理。

  • buildingTag:如果pipeline所执行的代码被打了tag,则执行:
复制代码
 when{

    
     buildingTag()
    
 }
    
    
    
    
    AI助手
  • tag:如果pipeline所执行的代码被打了tag,且tag名称符合规则,则执行:
复制代码
 when {

    
     tag "release-*"
    
 }
    
    
    
    
    AI助手

如果tag的参数为空,即tag(),则表示不论tag名称是什么都执行,与buildingTag的效果相同。

tag条件支持comparator参数,支持的值如下。

  • EQUALS:简单的文本比较。
复制代码
 when {

    
     tag pattern: "release-3.1", comparator: "EQUALS"
    
 }
    
    
    
    
    AI助手
  • GLOB (默认值):Ant风格路径表达式。由于是默认值,所以使用时一般省略。完整写法如下:
复制代码
 when {

    
     tag pattern: "release-*", comparator: "GLOB"
    
 }
    
    
    
    
    AI助手
  • REGEXP:正则表达式。使用方法如下:
复制代码
 when {

    
     tag pattern: "release-\ d+", comparator: "REGEXP"
    
 }
    
    
    
    
    AI助手

tag条件块非常适合根据tag进行发布的发布模式。

以上介绍的都是单条件判断,when指令还可以进行多条件组合判断。

  • allOf:所有条件都必须符合。下例表示当分支为master且环境变量DEPLOY_TO的值为production时,才符合条件。
复制代码
 when {

    
     allof {
    
     branch 'master';
    
     environment name: 'DEPLOY_TO', value: 'production'
    
     }
    
 }
    
    
    
    
    AI助手

注意,多条件之间使用分号分隔。

• anyOf:其中一个条件为true,就符合。下例表示master分支或staging分支都符合条件。

复制代码
 when {

    
     anyof {
    
     branch 'master';
    
     branch 'staging 1 
    
     }
    
 }
    
 
    
    
    AI助手

对于GitLab来说,并没有Jenkins多分支pipeline的概念,所以GitLab只会触发Jenkins进行分支索引(branch index),Jenkins可根据索引结果决定是否执行构建。对于多分支pipeline,Jenkins GitLab插件只监听push事件,不监听merge request事件。

而在Jenkins多分支pipeline项目的设置页面中,是找不到GitLab配置项的。只能通过修改Jenkinsfile来实现,在triggers指令中加入gitlab配置。

复制代码
 when {

    
     anyof {
    
     branch 'master';
    
     branch 'staging' 
    
     }
    
 }
    
    
    
    
    AI助手

值得一提的是,对于不同的分支使用不同的secretToken时,是以master分支的secretToken为准的。

在多分支pipeline场景下,我们希望触发某个分支的构建执行,GenericTrigger可以这么传参:

复制代码
 triggers {

    
     GenericTrigger(
    
     genericVariables: [
    
         [key: 'ref', value: '$.ref']
    
     ],
    
  
    
     token: env.JOB_NAME, 
    
     regexpFilterText:'$ref',
    
     regexpFilterExpression: 'refs/heads/' + env.BRANCH NAME
    
     )
    
 }
    
    
    
    
    AI助手

env.BRANCH NAME为当前pipeline的分支名。

十三、流水线项目构建实战

1、SpringBoot项目流水线构建

1. 创建Jenkins项目

  • serviceName:服务的名称;
  • buildShell:项目打包命令;
  • targetHosts:应用发布主机(需安装salt-minion);
  • targetDir:应用的发布目录;
  • user:发布用户;
  • port:应用的启动端口;

Jenkinsfile:

复制代码
 String buildShell = "${env.buildShell}"

    
 String targetHosts = "${env.targetHosts}"
    
 String targetDir = "${env.targetDir}"
    
 String serviceName = "${env.serviceName}"
    
 String user = "${env.user}"
    
 String port = "${env.port}"
    
 def jarName
    
  
    
 node("master"){
    
     //检出代码
    
     stage("checkout"){
    
     checkout scm
    
     }
    
     
    
     //执行构建
    
     stage("build"){
    
     def mvnHome = tool 'M3'
    
     sh " ${mvnHome}/bin/mvn ${buildShell} "
    
     
    
     jarName = sh returnStdout: true, script: "cd target && ls *.jar"
    
     jarName = jarName - "\n"
    
     //归档制品
    
     sh "mkdir -p /srv/salt/${serviceName} && mv  service.sh target/${jarName} /srv/salt/${serviceName} "
    
     }
    
     
    
     //发布应用
    
     stage("deploy"){
    
     sh " salt ${targetHosts} cmd.run ' rm -fr  ${targetDir}/*.jar '"
    
     sh " salt ${targetHosts} cp.get_file salt://${serviceName}/${jarName}  ${targetDir}/${jarName} mkdirs=True"
    
     sh " salt ${targetHosts} cp.get_file salt://${serviceName}/service.sh  ${targetDir}/service.sh mkdirs=True"
    
     sh " salt ${targetHosts} cmd.run 'chown ${user}:${user} ${targetDir} -R '"
    
     sh " salt ${targetHosts} cmd.run 'su - ${user} -c \"cd ${targetDir} &&  sh service.sh stop\" ' "
    
     sh " salt ${targetHosts} cmd.run 'su - ${user} -c \"cd ${targetDir} &&  sh service.sh start ${jarName} ${port} ${targetDir}\" ' "
    
     }
    
  
    
  
    
 }
    
  
    
    
    
    
    AI助手

服务启动脚本:

复制代码
 #!/bin/bash

    
  
    
 app=$2
    
 port=$3
    
 targetDir=$4
    
  
    
 start(){
    
     cd ${targetDir}
    
     nohup java -jar ${app} --server.port=${port} >>/dev/null 2>&1& echo $! > service.pid
    
     cd -
    
     
    
 }
    
  
    
 stop(){
    
     pid=`cat service.pid`
    
     if [ -z $pid ]
    
     then 
    
     echo "pid"
    
     else
    
     kill -9 ${pid}
    
     kill -9 ${pid}
    
     kill -9 ${pid}
    
     fi
    
 }
    
  
    
 case $1 in
    
 	start)
    
 	    start
    
 	    ;;
    
 	stop)
    
 	    stop
    
 	    ;;
    
 	    
    
 	restart)
    
 	    stop
    
 	    sleep 5
    
 	    start
    
 	    ;;
    
 	*)
    
 	    echo "[start|stop|restart]"
    
 	    ;;
    
     
    
 esac
    
    
    
    
    AI助手

构建日志:

复制代码
 Started by user admin

    
 Obtained Jenkinsfile from git https://github.com/zeyangli/springboot-helloworld.git
    
 Running in Durability level: MAX_SURVIVABILITY
    
 [Pipeline] Start of Pipeline
    
 [Pipeline] node
    
 Running on Jenkins in /var/lib/jenkins/workspace/demo/demo-springboot-service
    
 [Pipeline] {
    
 [Pipeline] stage
    
 [Pipeline] { (checkout)
    
 [Pipeline] checkout
    
 using credential 24982560-17fc-4589-819b-bc5bea89da77
    
  > /root/bin/git rev-parse --is-inside-work-tree # timeout=10
    
 Fetching changes from the remote Git repository
    
  > /root/bin/git config remote.origin.url https://github.com/zeyangli/springboot-helloworld.git # timeout=10
    
 Fetching upstream changes from https://github.com/zeyangli/springboot-helloworld.git
    
  > /root/bin/git --version # timeout=10
    
 using GIT_ASKPASS to set credentials gitlab
    
  > /root/bin/git fetch --tags --progress https://github.com/zeyangli/springboot-helloworld.git +refs/heads/*:refs/remotes/origin/*
    
  > /root/bin/git rev-parse refs/remotes/origin/master^{commit} # timeout=10
    
  > /root/bin/git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10
    
 Checking out Revision 552e73b0d8c9ef58f131b57f8e35c436f272ce14 (refs/remotes/origin/master)
    
  > /root/bin/git config core.sparsecheckout # timeout=10
    
  > /root/bin/git checkout -f 552e73b0d8c9ef58f131b57f8e35c436f272ce14
    
 Commit message: "Update Jenkinsfile"
    
  > /root/bin/git rev-list --no-walk 974b431aa8579f896e182e34d1b6fba895129b7d # timeout=10
    
 [Pipeline] }
    
 [Pipeline] // stage
    
 [Pipeline] stage
    
 [Pipeline] { (build)
    
 [Pipeline] tool
    
 [Pipeline] sh
    
 + /usr/local/apache-maven-3.6.0/bin/mvn clean install -DskipTests
    
 [INFO] Scanning for projects...
    
 [INFO] 
    
 [INFO] -----------------------< com.gazgeek:helloworld >-----------------------
    
 [INFO] Building helloworld 0.0.1-SNAPSHOT
    
 [INFO] --------------------------------[ jar ]---------------------------------
    
 [INFO] 
    
 [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ helloworld ---
    
 [INFO] Deleting /var/lib/jenkins/workspace/demo/demo-springboot-service/target
    
 [INFO] 
    
 [INFO] --- jacoco-maven-plugin:0.7.2.201409121644:prepare-agent (prepare-agent) @ helloworld ---
    
 [INFO] argLine set to -javaagent:/root/.m2/repository/org/jacoco/org.jacoco.agent/0.7.2.201409121644/org.jacoco.agent-0.7.2.201409121644-runtime.jar=destfile=/var/lib/jenkins/workspace/demo/demo-springboot-service/target/jacoco.exec
    
 [INFO] 
    
 [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ helloworld ---
    
 [INFO] Using 'UTF-8' encoding to copy filtered resources.
    
 [INFO] Copying 1 resource
    
 [INFO] Copying 0 resource
    
 [INFO] 
    
 [INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ helloworld ---
    
 [INFO] Changes detected - recompiling the module!
    
 [INFO] Compiling 2 source files to /var/lib/jenkins/workspace/demo/demo-springboot-service/target/classes
    
 [INFO] 
    
 [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ helloworld ---
    
 [INFO] Using 'UTF-8' encoding to copy filtered resources.
    
 [INFO] skip non existing resourceDirectory /var/lib/jenkins/workspace/demo/demo-springboot-service/src/test/resources
    
 [INFO] 
    
 [INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ helloworld ---
    
 [INFO] Changes detected - recompiling the module!
    
 [INFO] Compiling 3 source files to /var/lib/jenkins/workspace/demo/demo-springboot-service/target/test-classes
    
 [INFO] 
    
 [INFO] --- maven-surefire-plugin:2.17:test (default-test) @ helloworld ---
    
 [INFO] Tests are skipped.
    
 [INFO] 
    
 [INFO] --- maven-jar-plugin:2.5:jar (default-jar) @ helloworld ---
    
 [INFO] Building jar: /var/lib/jenkins/workspace/demo/demo-springboot-service/target/helloworld-0.0.1-SNAPSHOT.jar
    
 [INFO] 
    
 [INFO] --- spring-boot-maven-plugin:1.2.2.RELEASE:repackage (default) @ helloworld ---
    
 [INFO] 
    
 [INFO] --- maven-install-plugin:2.5.2:install (default-install) @ helloworld ---
    
 [INFO] Installing /var/lib/jenkins/workspace/demo/demo-springboot-service/target/helloworld-0.0.1-SNAPSHOT.jar to /root/.m2/repository/com/gazgeek/helloworld/0.0.1-SNAPSHOT/helloworld-0.0.1-SNAPSHOT.jar
    
 [INFO] Installing /var/lib/jenkins/workspace/demo/demo-springboot-service/pom.xml to /root/.m2/repository/com/gazgeek/helloworld/0.0.1-SNAPSHOT/helloworld-0.0.1-SNAPSHOT.pom
    
 [INFO] ------------------------------------------------------------------------
    
 [INFO] BUILD SUCCESS
    
 [INFO] ------------------------------------------------------------------------
    
 [INFO] Total time:  5.497 s
    
 [INFO] Finished at: 2019-04-05T07:41:23+08:00
    
 [INFO] ------------------------------------------------------------------------
    
 [Pipeline] sh
    
 + cd target
    
 + ls helloworld-0.0.1-SNAPSHOT.jar
    
 [Pipeline] sh
    
 + mkdir -p /srv/salt/demo-springboot-service
    
 + mv service.sh target/helloworld-0.0.1-SNAPSHOT.jar /srv/salt/demo-springboot-service
    
 [Pipeline] }
    
 [Pipeline] // stage
    
 [Pipeline] stage
    
 [Pipeline] { (deploy)
    
 [Pipeline] sh
    
 + salt VM_0_12_centos cmd.run ' rm -fr  /opt/javatest/*.jar '
    
 VM_0_12_centos:
    
 [Pipeline] sh
    
 + salt VM_0_12_centos cp.get_file salt://demo-springboot-service/helloworld-0.0.1-SNAPSHOT.jar /opt/javatest/helloworld-0.0.1-SNAPSHOT.jar mkdirs=True
    
 VM_0_12_centos:
    
     /opt/javatest/helloworld-0.0.1-SNAPSHOT.jar
    
 [Pipeline] sh
    
 + salt VM_0_12_centos cp.get_file salt://demo-springboot-service/service.sh /opt/javatest/service.sh mkdirs=True
    
 VM_0_12_centos:
    
     /opt/javatest/service.sh
    
 [Pipeline] sh
    
 + salt VM_0_12_centos cmd.run 'chown tomcat:tomcat /opt/javatest -R '
    
 VM_0_12_centos:
    
 [Pipeline] sh
    
 + salt VM_0_12_centos cmd.run 'su - tomcat -c "cd /opt/javatest &&  sh service.sh stop" '
    
 VM_0_12_centos:
    
 [Pipeline] sh
    
 + salt VM_0_12_centos cmd.run 'su - tomcat -c "cd /opt/javatest &&  sh service.sh start helloworld-0.0.1-SNAPSHOT.jar 8080 /opt/javatest" '
    
 VM_0_12_centos:
    
     /opt/javatest
    
 [Pipeline] }
    
 [Pipeline] // stage
    
 [Pipeline] }
    
 [Pipeline] // node
    
 [Pipeline] End of Pipeline
    
 Finished: SUCCESS
    
    
    
    
    AI助手

2、前端项目流水线构建

1. NodeJs 前端发布流水线

生产部署服务器 production-server 已经安装了Nginx,Jenkins上安装NodeJS插件。

Jenkins配置NodeJS服务器:

Manage Jenkins(系统管理) -> Global Tool Configuration(全局工具配置) -> NodeJS。

在Jenkins上创建前端流水线项目tensquare-ui :


编写Jenkinsfile构建脚本:

复制代码
 #!groovy

    
  
    
  
    
 //Getcode
    
 String srcUrl = "${env.srcUrl}".trim()
    
 String branchName = "${env.branchName}".trim()
    
  
    
 //Global 
    
 String workspace = "/opt/jenkins/workspace"
    
 String targetHosts = "${env.targetHosts}".trim()
    
 String credentialsId = "24982560-17fc-4589-819b-bc5bea89da77"
    
 String serviceName = "${env.serviceName}".trim()
    
 String port = "${env.port}".trim()
    
 String user = "${env.user}".trim()
    
 String targetDir = "${env.targetDir}".trim()
    
   
    
  
    
 //Build
    
 String buildShell = "${env.buildShell}".trim()
    
  
    
 //代码检出
    
 def GetCode(srcUrl,branchName,credentialsId) {
    
     checkout([$class: 'GitSCM', branches: [[name: "${pathName}"]], 
    
     doGenerateSubmoduleConfigurations: false, 
    
     extensions: [], submoduleCfg: [], 
    
     userRemoteConfigs: [[credentialsId: "${credentialsId}", 
    
     url: "${srcUrl}"]]])
    
 }
    
  
    
  
    
 //Pipeline
    
  
    
 ansiColor('xterm') {
    
     node("master"){
    
     ws("${workspace}") {
    
         //Getcode
    
         stage("GetCode"){
    
             GetCode(srcUrl,branchName,credentialsId)
    
         }
    
         
    
         //Build
    
         stage("RunBuild"){
    
             sh """ 
    
                 ${buildShell} 
    
                 cd dist && tar zcf ${serviceName}.tar.gz * 
    
                 mkdir -p /srv/salt/${JOB_NAME}/
    
                 rm -fr /srv/salt/${JOB_NAME}/*
    
                 mv ${serviceName}.tar.gz /srv/salt/${JOB_NAME}/
    
                 
    
                """
    
         }
    
         
    
         
    
         //Deploy
    
         stage("RunDeploy"){
    
             sh """ 
    
                 salt ${targetHosts} cmd.run "rm -fr ${targetDir}/*"
    
                 salt ${targetHosts} cp.get_file "salt://${JOB_NAME}/${serviceName}.tar.gz ${targetDir}/${serviceName}.tar.gz makedirs=True "
    
                 salt ${targetHosts} cmd.run "cd ${targetDir} && tar zxf ${serviceName}.tar.gz "
    
                 salt ${targetHosts} cmd.run "chown ${user}:${user} ${targetDir} -R  "
    
                 salt ${targetHosts} cmd.run "ls -l "
    
             
    
    
    
                """
    
         
    
         }
    
     }              
    
     }       
    
 }
    
    
    
    
    AI助手

2. dotnet项目发布

复制代码
 rpm --import https://packages.microsoft.com/keys/microsoft.asc

    
  
    
 [root@VM_0_12_centos ~]# cat /etc/yum.repos.d/dotnetdev.repo 
    
 [packages-microsoft-com-prod]
    
 name=packages-microsoft-com-prod 
    
 baseurl= https://packages.microsoft.com/yumrepos/microsoft-rhel7.3-prod
    
 enabled=1
    
 gpgcheck=1
    
 gpgkey=https://packages.microsoft.com/keys/microsoft.asc
    
  
    
 yum install libunwind libicu
    
 yum install dotnet-sdk-2.1.103
    
  
    
 dotnet --version
    
    
    
    
    AI助手

创建Jenkins项目:

Jenkinsfile:

复制代码
  
    
 String buildShell = "${env.buildShell}"
    
 String targetDir  = "${env.targetDir}"
    
  
    
 node("runner"){
    
     stage("checkout"){
    
     checkout scm
    
     }
    
     
    
     
    
     stage("build"){
    
     sh " ${buildShell} "
    
     }
    
     
    
     
    
     stage("publish"){
    
     sh " mkdir -p ${targetDir} "
    
     sh " dotnet publish -o ${targetDir}  && ls -l ${targetDir}"
    
     
    
     }
    
 }
    
    
    
    
    AI助手

构建日志:

复制代码
 成功 Console Output

    
 Started by user admin
    
 Obtained new-jenkinsfile from git https://github.com/zeyangli/dotnet-HelloWorld.git
    
 Running in Durability level: MAX_SURVIVABILITY
    
 [Pipeline] Start of Pipeline
    
 [Pipeline] node
    
 Running on runner in /var/lib/jenkins/workspace/demo/demo-dotnet-service
    
 [Pipeline] {
    
 [Pipeline] stage
    
 [Pipeline] { (checkout)
    
 [Pipeline] checkout
    
 using credential 24982560-17fc-4589-819b-bc5bea89da77
    
 Fetching changes from the remote Git repository
    
  > /root/bin/git rev-parse --is-inside-work-tree # timeout=10
    
  > /root/bin/git config remote.origin.url https://github.com/zeyangli/dotnet-HelloWorld.git # timeout=10
    
 Fetching upstream changes from https://github.com/zeyangli/dotnet-HelloWorld.git
    
  > /root/bin/git --version # timeout=10
    
 using GIT_ASKPASS to set credentials gitlab
    
  > /root/bin/git fetch --tags --progress https://github.com/zeyangli/dotnet-HelloWorld.git +refs/heads/*:refs/remotes/origin/*
    
 Checking out Revision ed6460aaa029e1a29654c3eec369ec97783a8fb1 (refs/remotes/origin/master)
    
 Commit message: "Update new-jenkinsfile"
    
 [Pipeline] }
    
 [Pipeline] // stage
    
 [Pipeline] stage
    
 [Pipeline] { (build)
    
 [Pipeline] sh
    
  > /root/bin/git rev-parse refs/remotes/origin/master^{commit} # timeout=10
    
  > /root/bin/git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10
    
  > /root/bin/git config core.sparsecheckout # timeout=10
    
  > /root/bin/git checkout -f ed6460aaa029e1a29654c3eec369ec97783a8fb1
    
  > /root/bin/git rev-list --no-walk 75976c755992589ce9ebb4799d623e6ed3447a2e # timeout=10
    
 + dotnet restore
    
   Restore completed in 107.58 ms for /var/lib/jenkins/workspace/demo/demo-dotnet-service/HelloWorld/HelloWorld.csproj.
    
 + dotnet build
    
 Microsoft (R) Build Engine version 15.6.82.30579 for .NET Core
    
 Copyright (C) Microsoft Corporation. All rights reserved.
    
  
    
   Restore completed in 49.87 ms for /var/lib/jenkins/workspace/demo/demo-dotnet-service/HelloWorld/HelloWorld.csproj.
    
   HelloWorld -> /var/lib/jenkins/workspace/demo/demo-dotnet-service/HelloWorld/bin/Debug/netcoreapp2.0/HelloWorld.dll
    
  
    
 Build succeeded.
    
     0 Warning(s)
    
     0 Error(s)
    
  
    
 Time Elapsed 00:00:02.30
    
 [Pipeline] }
    
 [Pipeline] // stage
    
 [Pipeline] stage
    
 [Pipeline] { (publish)
    
 [Pipeline] sh
    
 + mkdir -p /opt/dotnet
    
 [Pipeline] sh
    
 + dotnet publish -o /opt/dotnet
    
 Microsoft (R) Build Engine version 15.6.82.30579 for .NET Core
    
 Copyright (C) Microsoft Corporation. All rights reserved.
    
  
    
   Restore completed in 54 ms for /var/lib/jenkins/workspace/demo/demo-dotnet-service/HelloWorld/HelloWorld.csproj.
    
   HelloWorld -> /var/lib/jenkins/workspace/demo/demo-dotnet-service/HelloWorld/bin/Debug/netcoreapp2.0/HelloWorld.dll
    
   HelloWorld -> /opt/dotnet/
    
 + ls -l /opt/dotnet
    
 total 20
    
 -rw-r--r-- 1 root root  440 Apr  4 21:03 HelloWorld.deps.json
    
 -rw-r--r-- 1 root root 7168 Apr  4 21:02 HelloWorld.dll
    
 -rw-r--r-- 1 root root 1364 Apr  4 21:02 HelloWorld.pdb
    
 -rw-r--r-- 1 root root  146 Apr  4 21:03 HelloWorld.runtimeconfig.json
    
 [Pipeline] }
    
 [Pipeline] // stage
    
 [Pipeline] }
    
 [Pipeline] // node
    
 [Pipeline] End of Pipeline
    
 Finished: SUCCESS
    
    
    
    
    AI助手

3. 静态资源发布

项目配置部分主要是将网站源代码上传到github,然后搭建用户访问的web服务器。再经过Jenkins配置发布代码到web服务器。

安装Nginx服务:

复制代码
 yum -y install nginx

    
 service nginx start 
    
 chkconfig nginx on
    
    
    
    
    AI助手

创建站点目录:

复制代码
    mkdir -p /opt/nginx/myweb
    
    AI助手

配置Nginx:

复制代码
 vim /etc/nginx/conf.d/default.conf

    
  
    
 server {
    
     listen       80 default_server;
    
     server_name  www.xxxxx.com;
    
  
    
     include /etc/nginx/default.d/*.conf;
    
  
    
     location / {
    
     root /opt/nginx/myweb;
    
     index index.html ;
    
     }
    
     
    
     error_page 404 /404.html;
    
     location = /40x.html {
    
     }
    
  
    
     error_page 500 502 503 504 /50x.html;
    
     location = /50x.html {
    
     }
    
  
    
 }
    
  
    
 service nginx restart
    
    
    
    
    AI助手

编写Jenkinsfile:

复制代码
 #!groovy

    
 @Library('devops-demo') _          //导入sharelibrary
    
  
    
 def tools = new org.devops.tools()    
    
  
    
  
    
 //Getcode
    
 String srcUrl = "${env.srcUrl}".trim()
    
 String srcType = "${env.srcType}".trim()
    
 String branchName = "${env.branchName}".trim()
    
 String tagName = "${env.tagName}".trim()
    
 String moduleName = "${env.moduleName}".trim()
    
  
    
 //Global 
    
 String workspace = "/opt/jenkins/workspace"
    
 String targetHosts = "${env.targetHosts}".trim()
    
 String jobType = "${JOB_NAME}".split('_')[-1]
    
 String credentialsId = "24982560-17fc-4589-819b-bc5bea89da77"
    
 String serviceName = "${env.serviceName}".trim()
    
 String javaVersion = "${env.javaVersion}".trim()
    
 String dependency = "${env.dependency}".trim()
    
 String port = "${env.port}".trim()
    
 String user = "${env.user}".trim()
    
 String targetDir = "${env.targetDir}".trim()
    
 def runserver 
    
 def buildDir = tools.BuildDir(workspace,srcType,tagName,moduleName)[0]  
    
 def srcDir = tools.BuildDir(workspace,srcType,tagName,moduleName)[1]  
    
  
    
 //Build
    
 String midwareType = "${env.midwareType}".trim()
    
 String buildType = "${env.buildType}".trim()
    
 String buildShell = "${env.buildShell}".trim()
    
  
    
 //Pipeline
    
  
    
 ansiColor('xterm') {
    
     node("master"){
    
     ws("${workspace}") {
    
         //Getcode
    
         stage("GetCode"){
    
             tools.PrintMes('获取代码','green')
    
             try {
    
                 def getcode = new org.devops.getcode()
    
                 getcode.GetCode(srcType,srcUrl,tagName,branchName,credentialsId)
    
             } catch(e){
    
             
    
             }    
    
         }
    
         
    
         //Build
    
         stage("RunBuild"){
    
             tools.PrintMes('应用打包','green')
    
             def build = new org.devops.build()
    
     
    
             try {
    
                 if ("${midwareType}" == "Nginx"){
    
                     build.WebBuild(srcDir,serviceName)
    
                 
    
                 } else if ("${midwareType}" == "NodeJs"){
    
                     def webDist=srcDir + '/dist'
    
                     sh " cd ${srcDir} && ${buildShell} && cd -"
    
                     build.WebBuild(webDist,serviceName)
    
                 }
    
                 else {
    
                     build.Build(javaVersion,buildType,buildDir,buildShell)
    
                 }
    
             }catch(e){
    
                 currentBuild.description='运行打包失败!'
    
                 error '运行打包失败!'
    
             }
    
         }
    
         
    
         
    
         //Deploy
    
         stage("RunDeploy"){
    
             tools.PrintMes('发布应用','green')
    
             def deploy = new org.devops.deploy()
    
             
    
             switch("${midwareType}"){
    
                 case 'SpringBoot':
    
                     deploy.SpringBootInit(javaOption,dependency,credentialsId)
    
                     deploy.JavaDeploy('SpringBoot','jar',srcDir,user,targetHosts,targetDir+"/${serviceName}",port)
    
                     break;
    
                     
    
                 case 'Tomcat':
    
                     def tomcatDir=targetDir + "/${port}/webapps/"
    
                     deploy.JavaDeploy('Tomcat','war',srcDir,user,targetHosts,tomcatDir,port)
    
                     break;
    
                     
    
                 case 'NodeJs':
    
                     deploy.WebDeploy(user,serviceName,targetDir)
    
                     break;
    
  
    
                 case 'Nginx': 
    
                     deploy.WebDeploy(user,serviceName,targetDir)
    
                     break;
    
  
    
                 default:
    
                     error "中间件类型错误!"  
    
             }
    
         }
    
     }
    
     
    
     }
    
 }
    
    
    
    
    AI助手

org/src/devops/build.groovy:

复制代码
 package org.devops

    
  
    
 //构建打包
    
 def Build(javaVersion,buildType,buildDir,buildShell){
    
     if (buildType == 'maven'){
    
     Home = tool '/usr/local/apache-maven'
    
     buildHome = "${Home}/bin/mvn"
    
     } else if (buildType == 'ant'){
    
     Home = tool 'ANT'
    
     buildHome = "${Home}/bin/ant"
    
     } else if (buildType == 'gradle'){
    
     buildHome = '/usr/local/bin/gradle'
    
     } else{
    
     error 'buildType Error [maven|ant|gradle]'
    
     }
    
     echo "BUILD_HOME: ${buildHome}"
    
     
    
     //选择JDK版本
    
     jdkPath = ['jdk7' : '/usr/local/jdk1.7.0_79',
    
            'jdk6' : '/usr/local/jdk1.6.0_45',
    
            'jdk8' : '/usr/java/jdk1.8.0_111',
    
            'jdk11': '/usr/local/jdk-11.0.1',
    
            'null' : '/usr/java/jdk1.8.0_111']
    
     def javaHome = jdkPath["$javaVersion"]
    
     if ("$javaVersion" == 'jdk11'){
    
    sh  """
    
     export JAVA_HOME=${javaHome}
    
     export PATH=\$JAVA_HOME/bin:\$PATH
    
     java -version
    
     cd ${buildDir} && ${buildHome} ${buildShell}
    
     """
    
     } else {
    
     sh  """
    
         export JAVA_HOME=${javaHome}
    
         export PATH=\$JAVA_HOME/bin:\$PATH
    
         export CLASSPATH=.:\$JAVA_HOME/lib/dt.jar:\$JAVA_HOME/lib/tools.jar
    
         java -version
    
         cd ${buildDir} && ${buildHome} ${buildShell}
    
         """
    
     }
    
 }
    
  
    
  
    
 //前端Build
    
 def WebBuild(srcDir,serviceName){
    
     def deployPath = "/srv/salt/${JOB_NAME}"
    
     sh """
    
     [ -d ${deployPath} ] || mkdir -p ${deployPath}
    
     cd ${srcDir}/
    
     rm -fr *.tar.gz 
    
     tar zcf ${serviceName}.tar.gz * 
    
     cp ${serviceName}.tar.gz ${deployPath}/${serviceName}.tar.gz
    
     cd -
    
     """
    
 }
    
    
    
    
    AI助手

org/src/devops/codescan.groovy:

复制代码
 package org.devops

    
  
    
  
    
 //代码扫描
    
 def SonarScan(projectType,skipSonar,srcDir,serviceName,scanDir){
    
     def scanHome = "/usr/local/sonar-scanner"
    
     if (projectType == 'java'){
    
     if ("${buildType}" == 'gradle'){
    
         codepath = 'build/classes'
    
     } else{
    
         codepath = 'target/classes'
    
     }
    
     try {
    
         sh """
    
             cd ${srcDir} 
    
             ${scanHome}/bin/sonar-scanner -Dsonar.projectName=${serviceName} -Dsonar.projectKey=${serviceName}  \
    
             -Dsonar.sources=.  -Dsonar.language=java -Dsonar.sourceEncoding=UTF-8 \
    
             -Dsonar.java.binaries=${codepath} -Dsonar.java.coveragePlugin=jacoco \
    
             -Dsonar.jacoco.reportPath=target/jacoco.exec -Dsonar.junit.reportsPath=target/surefire-reports \
    
             -Dsonar.surefire.reportsPath=target/surefire-reports -Dsonar.projectDescription='devopsdevops'
    
          """ 
    
     } catch (e){
    
         currentBuild.description="代码扫描失败!"
    
         error '代码扫描失败!'
    
     }
    
     } else if (projectType == 'web'){
    
     try {
    
         sh  """
    
             cd ${srcDir} 
    
             ${scanHome}/bin/sonar-scanner -Dsonar.projectName=${serviceName} \
    
             -Dsonar.projectKey=${serviceName} -Dsonar.sources=${scanDir} -Dsonar.language=js  
    
             cd - 
    
             """
    
     } catch (e){
    
         currentBuild.description="代码扫描失败!"
    
         error '代码扫描失败!'
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

org/src/devops/deploy.groovy:

复制代码
 package org.devops

    
  
    
  
    
 //saltapi模板
    
 def Salt(salthost,saltfunc,saltargs) {
    
     /*result = salt(authtype: 'pam', 
    
             clientInterface: local( arguments: saltargs,
    
                                     function: saltfunc, 
    
                                     target: salthost, 
    
                                     targettype: 'list'),
    
             credentialsId: "f89abde3-49f0-4b75-917e-c4e49c483f4f", 
    
             servername: "http://127.0.0.1:9000")*/
    
     
    
     sh """
    
     salt ${salthost} ${saltfunc} ${saltargs}
    
     """
    
     //println(result)
    
     //PrintMes(result,'blue')
    
     //return  result
    
 }
    
  
    
  
    
 //前端类型发布
    
 def WebDeploy(user,serviceName,targetDir){
    
     try {
    
     println('清空发布目录')
    
     
    
     Salt(targetHosts,'cmd.run', "cmd=\" rm -fr  ${targetDir}/* \"")
    
     
    
     println('发布软件包')
    
     Salt(targetHosts,'cp.get_file', "salt://${JOB_NAME}/${serviceName}.tar.gz ${targetDir}/${serviceName}.tar.gz makedirs=True ")
    
     sleep 2;
    
     
    
     println('解压')
    
     Salt(targetHosts,'cmd.run', "cmd=\" cd ${targetDir} && tar zxf ${serviceName}.tar.gz  \"")
    
     sleep 2;
    
     
    
     println('授权')
    
     Salt(targetHosts,'cmd.run', "cmd=\"chown ${user}:${user} ${targetDir} -R  \"")
    
     sleep 2;
    
     println('获取发布文件')
    
     Salt(targetHosts,'cmd.run', "cmd=\" ls -l  ${targetDir} \"")
    
     
    
     println('删除缓存文件')
    
     sh "rm -fr /srv/salt/${JOB_NAME}/*"
    
     } catch (e){
    
     currentBuild.description='包发布失败!'
    
     error '包发布失败!'
    
     }
    
 }
    
    
    
    
    AI助手

org/src/devops/getcode.groovy:

复制代码
 package org.devops

    
  
    
 //代码检出
    
 def GetCode(srcType,srcUrl,tagName,branchName,credentialsId) {
    
     //delete 'origin/'
    
     if (branchName.startsWith('origin/')){
    
     branchName=branchName.minus("origin/")
    
     } 
    
     
    
     if(tagName == "null"){
    
     pathName = "*/${branchName}"
    
     }else{
    
     pathName = "refs/tags/${tagName}"
    
     }
    
     checkout([$class: 'GitSCM', branches: [[name: "${pathName}"]], 
    
     doGenerateSubmoduleConfigurations: false, 
    
     extensions: [], submoduleCfg: [], 
    
     userRemoteConfigs: [[credentialsId: "${credentialsId}", 
    
     url: "${srcUrl}"]]])
    
 }
    
    
    
    
    AI助手

org/src/devops/tools.groovy:

复制代码
 package org.devops

    
  
    
  
    
 //格式化输出
    
 def PrintMes(value,color){
    
     colors = ['red'   : "\033[40;31m >>>>>>>>>>>${value}<<<<<<<<<<< \033[0m",
    
           'blue'  : "\033[47;34m ${value} \033[0m",
    
           'green' : "�[1;32m>>>>>>>>>>${value}>>>>>>>>>>�[m",
    
           'green1' : "\033[40;32m >>>>>>>>>>>${value}<<<<<<<<<<< \033[0m" ]
    
     ansiColor('xterm') {
    
     println(colors[color])
    
     }
    
 }
    
  
    
  
    
 //获取源码目录
    
 def BuildDir(workspace,srcType,tagName,moduleName) {
    
     def srcDir = workspace
    
     if(srcType == "Git") {
    
     buildDir = "${workspace}"
    
     if(moduleName == "null"){
    
         srcDir = "${workspace}"
    
     }else{
    
         srcDir = "${workspace}/${moduleName}"
    
     }
    
     }else{
    
     if(tagName == "null") {
    
         def srcTmp = srcUrl.split("/")[-1]
    
         srcDir = "${workspace}/${srcTmp}"
    
     }else{
    
         srcDir = "${workspace}/${tagName}"
    
     }
    
     }
    
     buildDir = srcDir
    
     return [buildDir,srcDir]
    
 }
    
  
    
 //saltapi模板
    
 def Salt(salthost,saltfunc,saltargs) {
    
     result = salt(authtype: 'pam', 
    
             clientInterface: local( arguments: saltargs,
    
                                     function: saltfunc, 
    
                                     target: salthost, 
    
                                     targettype: 'list'),
    
             credentialsId: "c4ec3410-7f97-40fa-8ad9-be38a7bbbcd8", 
    
             servername: "http://127.0.0.1:8000")
    
     println(result)
    
     //PrintMes(result,'blue')
    
     return  result
    
 }
    
    
    
    
    AI助手

构建测试:

3、Go项目流水线构建

1. 安装开发环境

复制代码
 wget  https://dl.google.com/go/go1.10.2.linux-amd64.tar.gz

    
 tar zxf go1.10.2.linux-amd64.tar.gz -C /usr/local/
    
  
    
 vim /etc/profile
    
 export GO_PATH=/usr/local/go
    
 export PATH=$PATH:$GO_PATH/bin
    
  
    
 source /etc/profile
    
 go version
    
  
    
 useradd golang
    
    
    
    
    AI助手

2. Jenkins项目

  • serviceName: 服务名称;
  • buildShell: 构建命令;
  • targetHosts: 发布目标主机;
  • user: 执行用户;
  • targetDir: 发布目标主机的工作目录;

3. Jenkinsfile

将build完成的二进制文件、static、service.sh生产压缩包。 移动到发布目录,发布,解压包,启动服务。

复制代码
 String buildShell = "${env.buildShell}"

    
 String targetHosts = "${env.targetHosts}"
    
 String targetDir = "${env.targetDir}"
    
 String serviceName = "${env.serviceName}"
    
 String user = "${env.user}"
    
  
    
  
    
 node("master"){
    
     stage("checkout"){
    
     checkout scm
    
     }
    
     
    
     stage("build"){   
    
     sh """ 
    
            export GOPATH=/usr/local/go
    
            export PATH=$PATH:\$GOPATH/bin
    
            ${buildShell}
    
            mkdir -p /srv/salt/${serviceName} 
    
            tar zcf ${serviceName}.tar.gz main static service.sh 
    
            rm -fr /srv/salt/${serviceName}/*
    
            mv ${serviceName}.tar.gz /srv/salt/${serviceName} 
    
        """
    
     }
    
     
    
     stage("deploy"){
    
     sh " salt ${targetHosts} cmd.run ' rm -fr  ${targetDir}/* '"
    
     sh " salt ${targetHosts} cp.get_file salt://${serviceName}/${serviceName}.tar.gz  ${targetDir}/${serviceName}.tar.gz mkdirs=True"
    
     sh " salt ${targetHosts} cmd.run 'chown ${user}:${user} ${targetDir} -R '"
    
     sh " salt ${targetHosts} cmd.run 'su - ${user} -c \" cd ${targetDir} && tar zxf ${serviceName}.tar.gz \" '"
    
     sh " salt ${targetHosts} cmd.run 'su - ${user} -c \"cd ${targetDir} &&  sh service.sh stop\" ' "
    
     sh " salt ${targetHosts} cmd.run 'su - ${user} -c \"cd ${targetDir} &&  sh service.sh start ${targetDir}\" ' "
    
     }
    
  
    
  
    
 }
    
    
    
    
    AI助手

4. 服务控制脚本

复制代码
 #!/bin/bash

    
  
    
 targetDir=$2
    
  
    
 start(){
    
     cd ${targetDir}
    
     nohup ./main >>/dev/null 2>&1& echo $! > service.pid
    
     cd -
    
     
    
 }
    
  
    
  
    
 stop(){
    
     pid=`cat service.pid`
    
     if [ -z $pid ]
    
     then 
    
     echo "pid"
    
     else
    
     kill -9 ${pid}
    
     kill -9 ${pid}
    
     kill -9 ${pid}
    
     fi
    
 }
    
  
    
  
    
 case $1 in
    
 start)
    
     start
    
     ;;
    
 stop)
    
     stop
    
     ;;
    
     
    
 restart)
    
     stop
    
     sleep 5
    
     start
    
     ;;
    
 *)
    
     echo "[start|stop|restart]"
    
     ;;
    
     
    
 esac
    
    
    
    
    AI助手

5. 构建输出

复制代码
 Started by user admin

    
 Obtained Jenkinsfile from git https://github.com/zeyangli/golang-helloworld-web.git
    
 Running in Durability level: MAX_SURVIVABILITY
    
 [Pipeline] Start of Pipeline
    
 [Pipeline] node
    
 Running on Jenkins in /var/lib/jenkins/workspace/demo/demo-golang-service
    
 [Pipeline] {
    
 [Pipeline] stage
    
 [Pipeline] { (checkout)
    
 [Pipeline] checkout
    
 using credential 24982560-17fc-4589-819b-bc5bea89da77
    
  > /root/bin/git rev-parse --is-inside-work-tree # timeout=10
    
 Fetching changes from the remote Git repository
    
  > /root/bin/git config remote.origin.url https://github.com/zeyangli/golang-helloworld-web.git # timeout=10
    
 Fetching upstream changes from https://github.com/zeyangli/golang-helloworld-web.git
    
  > /root/bin/git --version # timeout=10
    
 using GIT_ASKPASS to set credentials gitlab
    
  > /root/bin/git fetch --tags --progress https://github.com/zeyangli/golang-helloworld-web.git +refs/heads/*:refs/remotes/origin/*
    
  > /root/bin/git rev-parse refs/remotes/origin/master^{commit} # timeout=10
    
  > /root/bin/git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10
    
 Checking out Revision 16a66ed8523442dd04bc6890fd30ff26455fa4c3 (refs/remotes/origin/master)
    
  > /root/bin/git config core.sparsecheckout # timeout=10
    
  > /root/bin/git checkout -f 16a66ed8523442dd04bc6890fd30ff26455fa4c3
    
 Commit message: "Update Jenkinsfile"
    
  > /root/bin/git rev-list --no-walk 09d6c9f794f1337f1ec0928106991efd5354ddf8 # timeout=10
    
 [Pipeline] }
    
 [Pipeline] // stage
    
 [Pipeline] stage
    
 [Pipeline] { (build)
    
 [Pipeline] sh
    
 + export GOPATH=/usr/local/go
    
 + GOPATH=/usr/local/go
    
 + export PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/go/bin
    
 + PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/go/bin
    
 + go clean
    
 warning: GOPATH set to GOROOT (/usr/local/go) has no effect
    
 + go build main.go
    
 warning: GOPATH set to GOROOT (/usr/local/go) has no effect
    
 + mkdir -p /srv/salt/demo-golang-service
    
 + tar zcf demo-golang-service.tar.gz main static service.sh
    
 + rm -fr /srv/salt/demo-golang-service/demo-golang-service.tar.gz
    
 + mv demo-golang-service.tar.gz /srv/salt/demo-golang-service
    
 [Pipeline] }
    
 [Pipeline] // stage
    
 [Pipeline] stage
    
 [Pipeline] { (deploy)
    
 [Pipeline] sh
    
 + salt VM_0_12_centos cmd.run ' rm -fr  /opt/go/* '
    
 VM_0_12_centos:
    
 [Pipeline] sh
    
 + salt VM_0_12_centos cp.get_file salt://demo-golang-service/demo-golang-service.tar.gz /opt/go/demo-golang-service.tar.gz mkdirs=True
    
 VM_0_12_centos:
    
     /opt/go/demo-golang-service.tar.gz
    
 [Pipeline] sh
    
 + salt VM_0_12_centos cmd.run 'chown golang:golang /opt/go -R '
    
 VM_0_12_centos:
    
 [Pipeline] sh
    
 + salt VM_0_12_centos cmd.run 'su - golang -c " cd /opt/go && tar zxf demo-golang-service.tar.gz " '
    
 VM_0_12_centos:
    
 [Pipeline] sh
    
 + salt VM_0_12_centos cmd.run 'su - golang -c "cd /opt/go &&  sh service.sh stop" '
    
 VM_0_12_centos:
    
     cat: service.pid: No such file or directory
    
     pid
    
 [Pipeline] sh
    
 + salt VM_0_12_centos cmd.run 'su - golang -c "cd /opt/go &&  sh service.sh start /opt/go" '
    
 VM_0_12_centos:
    
     /opt/go
    
 [Pipeline] }
    
 [Pipeline] // stage
    
 [Pipeline] }
    
 [Pipeline] // node
    
 [Pipeline] End of Pipeline
    
 Finished: SUCCESS
    
    
    
    
    AI助手

4、移动端项目流水线构建

1. 搭建Android打包环境(Centos)

安装JDK:

Java Downloads | Oracle

复制代码
 tar zxf jdk-8u201-linux-x64.tar.gz -C /usr/local

    
  
    
 #添加到/etc/profile
    
 export JAVA_HOME=/usr/local/jdk1.8.0_201
    
 export PATH=$PATH:$JAVA_HOME/bin
    
  
    
 source /etc/profile
    
  
    
 java -version
    
    
    
    
    AI助手

安装Android SDK Tools:

https://developer.android.com/studio/index.html

复制代码
 #解压后会有一个tools目录

    
 unzip  sdk-tools-linux-4333796.zip -d /usr/local
    
  
    
 export ANDROID_HOME=/usr/local/
    
 export PATH=$PATH:$ANDROID_HOME/tools/bin
    
  
    
 source /etc/profile
    
  
    
 sdkmanager --list  #验证环境变量配置准确
    
  
    
 [root@VM_7_14_centos ~]# sdkmanager --list | head -10
    
 [======================Warning: File /root/.android/repositories.cfg could not be loaded.
    
 Installed packages:=====================] 100% Computing updates...             
    
   Path                 | Version | Description                    | Location             
    
   -------              | ------- | -------                        | -------              
    
   build-tools;20.0.0   | 20.0.0  | Android SDK Build-Tools 20     | build-tools/20.0.0/  
    
   build-tools;23.0.1   | 23.0.1  | Android SDK Build-Tools 23.0.1 | build-tools/23.0.1/  
    
   build-tools;26.0.2   | 26.0.2  | Android SDK Build-Tools 26.0.2 | build-tools/26.0.2/  
    
   build-tools;28.0.3   | 28.0.3  | Android SDK Build-Tools 28.0.3 | build-tools/28.0.3/  
    
   platform-tools       | 28.0.2  | Android SDK Platform-Tools     | platform-tools/      
    
   platforms;android-19 | 4       | Android SDK Platform 19        | platforms/android-19/
    
   platforms;android-22 | 2       | Android SDK Platform 22        | platforms/android-22/
    
    
    
    
    AI助手

安装Gradle:

Gradle | Installation

复制代码
 unzip -d /usr/local gradle-5.3-bin.zip

    
 export GRADLE_HOME=/usr/local/gradle-5.3
    
 export PATH=$PATH:$GRADLE_HOME/bin
    
  
    
 source /etc/profile
    
  
    
 [root@VM_7_14_centos ~]# gradle -v
    
  
    
 ------------------------------------------------------------
    
 Gradle 5.3
    
 ------------------------------------------------------------
    
  
    
 Build time:   2019-03-20 11:03:29 UTC
    
 Revision:     f5c64796748a98efdbf6f99f44b6afe08492c2a0
    
  
    
 Kotlin:       1.3.21
    
 Groovy:       2.5.4
    
 Ant:          Apache Ant(TM) version 1.9.13 compiled on July 10 2018
    
 JVM:          1.8.0_201 (Oracle Corporation 25.201-b09)
    
 OS:           Linux 2.6.32-696.el6.x86_64 amd64
    
    
    
    
    AI助手

SDKmanager:

复制代码
 sdkmanager --list #获取已安装的和可用的包

    
 sdkmanager "platforms;android-28"  #安装和这个包
    
 sdkmanager --uninstall  "platforms;android-28"  #卸载这个包
    
    
    
    
    AI助手

2. 手动发布Android项目

构建打包:

目录:helloworld-android-gradle。

复制代码
 cd helloworld-android-gradle

    
 ./gradlew build
    
  
    
 Download https://jcenter.bintray.com/com/loopj/android/android-async-http/1.4.9/android-async-http-1.4.9.jar
    
 Download https://jcenter.bintray.com/cz/msebera/android/httpclient/4.3.6/httpclient-4.3.6.jar
    
 Download https://jcenter.bintray.com/com/google/code/gson/gson/2.8.1/gson-2.8.1.jar
    
 Download https://dl.google.com/dl/android/maven2/android/arch/lifecycle/common/1.0.0/common-1.0.0.jar
    
 Download https://dl.google.com/dl/android/maven2/android/arch/core/common/1.0.0/common-1.0.0.jar
    
 Unknown file extension: app/src/main/res/mipmap-xhdpi/ic_launcher.png
    
 Unknown file extension: app/src/main/res/mipmap-mdpi/ic_launcher.png
    
 Unknown file extension: app/src/main/res/mipmap-xxhdpi/ic_launcher.png
    
 Unknown file extension: app/src/main/res/mipmap-hdpi/ic_launcher.png
    
 Unknown file extension: app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
    
  
    
 > Task :app:lint
    
 Ran lint on variant release: 4 issues found
    
 Ran lint on variant debug: 4 issues found
    
 Wrote HTML report to file:///root/helloworld-android-gradle/app/build/reports/lint-results.html
    
 Wrote XML report to file:///root/helloworld-android-gradle/app/build/reports/lint-results.xml
    
  
    
  
    
 BUILD SUCCESSFUL in 1m 49s
    
 58 actionable tasks: 50 executed, 8 up-to-date
    
    
    
    
    AI助手

上传包到fir:

  • debug APK: helloworld-android-gradle/app/build/outputs/apk/debug
  • release APK: helloworld-android-gradle/app/build/outputs/apk/release

下载测试:

3. Android项目发布流水线

打包存放路径: 统一在app/build/outputs/apk/[debug|release]目录下。

编写上传包脚本(支持fim/pgyer):

fir.im平台发布应用API文档

蒲公英平台发布应用API文档

获取上传凭证: 获取cert.binary中的数据。

上传APK: 定义包信息并上传。

复制代码
 #coding:utf8

    
  
    
  
    
 import requests
    
 import sys
    
 import json
    
  
    
 from requests.packages.urllib3.exceptions import InsecureRequestWarning
    
 requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
    
  
    
 class ApkManage(object):
    
     def __init__(self):
    
     self.url = "http://api.fir.im/apps"
    
  
    
     def getCert(self):
    
     dataargs = {'type' : 'android',
    
                 'bundle_id' : bundleid,
    
                 'api_token' : apitoken}
    
  
    
     response = requests.post(self.url,data=dataargs)
    
     #print(response.status_code,response.text)
    
     cert = json.loads(response.text)
    
     #print(cert)
    
  
    
     return cert['cert']['binary']
    
  
    
     def uploadFir(self):
    
     certdata = self.getCert()
    
     
    
     try:
    
         print("upload apk to fir......")
    
         apkfile = {'file' : open(apkpath,'rb')}
    
         params = {"key"   : certdata['key'],
    
                   "token" : certdata['token'],
    
                   "x:name": appname ,
    
                   "x:build" : buildid,
    
                   "x:version" : appversion}
    
         response = requests.post(certdata['upload_url'],files=apkfile,data=params,verify=False)
    
         print(response.text)
    
         if int(response.status_code) == 200 :
    
             print("upload success!  return -->" + str(response.status_code))
    
         else:
    
             print("upload error! return -->" + str(response.status_code))
    
  
    
  
    
  
    
     except Exception as e:
    
         print("error: " + str(e))
    
  
    
  
    
     def uploadPgyer(self):
    
     url = 'https://qiniu-storage.pgyer.com/apiv1/app/upload'
    
     try:
    
         #print("upload apk to pgyer ......")
    
         apkfile = {'file' : open(apkpath,'rb')}
    
         params = {"uKey" : '7b70873bb4d6xxxxx1d2ae5',
    
                   "_api_key" : 'a9acab611e1xxxxxxx5cae360a5ab'}
    
  
    
         response = requests.post(url,files=apkfile,data=params,verify=False)
    
         #print(response.text)
    
         qrcodes = json.loads(response.text)['data']['appQRCodeURL']
    
         if int(response.status_code) == 200 :
    
             #print("upload success!  return -->" + str(response.status_code))
    
             print(qrcodes)
    
         else:
    
             print("upload error! return -->" + str(response.status_code))
    
  
    
     except Exception as e:
    
         raise
    
    
    
  
    
 if __name__ == '__main__':
    
     bundleid = sys.argv[1]
    
     apitoken = sys.argv[2]
    
     apkpath = sys.argv[3]
    
     appname = sys.argv[4]
    
     buildid = sys.argv[5]
    
     appversion = sys.argv[6]
    
     platform= sys.argv[7]
    
  
    
     server = ApkManage()
    
  
    
     if platform == 'fir':
    
     server.uploadFir()
    
     elif platform == 'pgyer':
    
     server.uploadPgyer()
    
    
    
    
    AI助手

使用方式:

复制代码
    python upapk.py demo-android-app-10 65d7edxxxxxxx7c4fabda25 app.apk  demo-android-app 10 10.12 fir
    
    AI助手

Jenkinsfile简单的包含三个stage,分别是:

  • Checkout: 检出代码(这种方式是直接获取Jenkinsfile的项目地址,Jenkinsfile在项目中可以这样写)。
  • Build: 构建打包 (执行gradle构建命令)。
  • Upload: 上传包到平台(更改包名,调用脚本上传)。
复制代码
 node("master"){

    
   stage("Checkout"){
    
     checkout scm
    
   }
    
  
    
   stage("Build"){
    
     sh 'chmod +x ./gradlew '
    
     sh " ${params.buildShell} "
    
   }
    
   
    
   stage("Upload"){
    
       /*sh """ 
    
      mv app/build/outputs/apk/debug/app-debug.apk ./${params.apkName}.apk
    
      python uploadapk.py ${params.bundleId} \
    
      ${params.apiToken} "${params.apkName}.apk" \
    
      "${params.apkName}" "${BUILD_ID}" \
    
      "${params.apkVersion}" "${params.appPlatform}"
    
      
    
      
    
      """*/
    
       sh "mv app/build/outputs/apk/debug/app-debug.apk ./${params.apkName}.apk"
    
       def result 
    
       result = sh returnStdout: true, script: """python uploadapk.py ${params.bundleId} \
    
                                              ${params.apiToken} "${params.apkName}.apk" \
    
                                              "${params.apkName}" "${BUILD_ID}" \
    
                                              "${params.apkVersion}" "${params.appPlatform}" """
    
    
    
       result = result - "\n"
    
       println(result)
    
     currentBuild.description="<img src=${result}>"
    
   }
    
   
    
   
    
 }
    
    
    
    
    AI助手

添加全局变量(android sdk):

导航->系统设置。

创建Pipeline:

这个项目因为Jenkinsfile和项目代码放在了一起,所以这个项目上的srcType、srcUrl、branchName参数暂时无效。

buildShell : 打包命令(debug|release)。

复制代码
  ./gradlew clean assembleDebug

    
  ./gradlew clean assembleRelease
    
    
    
    
    AI助手

bundleId: App的bundleId(发布新应用时必填)。

apiToken: 在fir.im平台创建。 获取用户token: 用户->apitoken

apkVersion : apk的版本。

apkName: apk的名称。

检出代码:

构建打包:

发布APK:

5、Docker 容器流水线构建

1. docker打包

1)后端打包

git代码拉取:

复制代码
 // 使用checkout拉取代码

    
 checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [], userRemoteConfigs: [[credentialsId: 'a8db6793-cc2b-4d82-bd3d-c5beb1c5149e', url: 'https://gitee.com/lvlinguang/rapid-demo-back.git']]])
    
  
    
 // 使用git branch拉取代码
    
 git branch: 'master', credentialsId: 'a8db6793-cc2b-4d82-bd3d-c5beb1c5149e', url: 'https://gitee.com/lvlinguang/rapid-demo-back.git'
    
    
    
    
    AI助手

mvn打包:

复制代码
    mvn -U clean install -Dmaven.test.skip=true
    
    AI助手

docker打包:

  • 使用maven插件打包并推送到Harbor;
复制代码
    mvn dockerfile:build dockerfile:push
    
    AI助手
  • 使用docker打包并推送到Harbor
复制代码
 // docker打包

    
 docker build -t 192.168.3.12:6007/library/rapid-demo-back:v2.1.2 .
    
  
    
 // harbor登录
    
 withCredentials([usernamePassword(credentialsId: '8d4ff3de-9b05-427d-a211-23be72f65bbb', passwordVariable: 'password', usernameVariable: 'username')]) {
    
     sh "docker login -u ${username} -p ${password} ${dockerRegistry}"
    
 }
    
  
    
 // docker推送到Harbor
    
 docker push 192.168.3.12:6007/library/rapid-demo-back:v2.1.2
    
    
    
    
    AI助手

删除本地镜像:

复制代码
    docker rmi 192.168.3.12:6007/library/rapid-demo-back:v2.1.2
    
    AI助手

Dockerfile内容:

复制代码
 # jdk版本

    
 FROM moxm/java:1.8-full
    
  
    
 # 临时文件目录
    
 VOLUME /tmp
    
  
    
 ADD target/*.jar app.jar
    
  
    
 ENTRYPOINT ["java","-jar","app.jar","-Xms512m","-Xmx1024m","-Djava.security.egd=file:/dev/./urandom"]
    
    
    
    
    AI助手

2)前端打包

checkout代码下载。

nodeJs打包:

  • 打包后目录下会生成dist文件。
复制代码
 nodejs('node-v12.20.0') {

    
     sh 'npm install --registry=https://registry.npm.taobao.org'
    
     sh 'npm run build'
    
 }
    
    
    
    
    AI助手

docker打包:

复制代码
    docker build -t 192.168.3.12:6007/library/rapid-demo-web:v2.1.2 .
    
    AI助手

推送到Harbor:

复制代码
 withCredentials([usernamePassword(credentialsId: '8d4ff3de-9b05-427d-a211-23be72f65bbb', passwordVariable: 'password', usernameVariable: 'username')]) {

    
     // harbor登录
    
     sh "docker login -u ${username} -p ${password} ${dockerRegistry}"
    
     // 推送Harbor
    
     docker push 192.168.3.12:6007/library/rapid-demo-web:v2.1.2
    
 }
    
    
    
    
    AI助手

删除本地镜像:

复制代码
    docker rmi 192.168.3.12:6007/library/rapid-demo-web:v2.1.2
    
    AI助手

Dockerfile内容:

复制代码
 FROM nginx:alpine

    
 # 作者信息
    
 MAINTAINER lvlinguang <635074566@qq.com>
    
 # 复制html页面
    
 COPY ./dist ./usr/share/nginx/html/
    
 # 复制nginx配置文件
    
 COPY ./default.conf /etc/nginx/conf.d/
    
    
    
    
    AI助手

default.conf:

复制代码
 server {

    
     listen       8080;
    
     #listen  [::]:80;
    
     server_name  localhost;
    
  
    
     gzip            on;
    
     gzip_min_length 1000;
    
     gzip_comp_level 6;
    
     gzip_types      text/plain application/xml application/javascript application/x-javascript text/css application/xml;
    
  
    
     gzip_vary on;
    
     
    
     location / {
    
     root   /usr/share/nginx/html;
    
     index  index.html index.htm;
    
     }
    
  
    
 # 后端接口代理
    
 #    location /screen {
    
 #      proxy_set_header Host $http_host;
    
 #      proxy_set_header X-Real-IP $remote_addr;
    
 #      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    
 #      proxy_set_header X-Forwarded-Proto $scheme;
    
 #      proxy_pass http://192.168.3.15:6032/;
    
 #    }
    
  
    
     error_page   500 502 503 504  /50x.html;
    
     location = /50x.html {
    
     root   /usr/share/nginx/html;
    
     }
    
 }
    
    
    
    
    AI助手

2. 启动容器

查询容器是否存在,存在则删除。

复制代码
 # 容器id

    
 containerId=$(docker ps -a | grep -w "rapid-demo-back" | awk '{print $1}')
    
 if [ "$containerId" != "" ] ;
    
 then
    
   #停掉容器
    
   docker stop "$containerId"
    
   #删除容器
    
   docker rm "$containerId"
    
 fi
    
    
    
    
    AI助手

查询镜像是否存在,存在则删除。

复制代码
 # 镜像id

    
 imageId=$(docker images | grep -w "rapid-demo-back" | awk '{print $3}')
    
 if [ "$imageId" != "" ] ;
    
 then
    
   #删除镜像
    
   docker rmi -f "$imageId"
    
 fi
    
    
    
    
    AI助手

下载镜像:

复制代码
    docker pull 192.168.3.12:6007/library/rapid-demo-back:v2.1.2
    
    AI助手

启动容器:

复制代码
    docker run -d --name rapid-demo-back --restart=always -p 6001:8080 192.168.3.12:6007/library/rapid-demo-back:v2.1.2
    
    AI助手

3. 完整代码

pipeline script:

复制代码
 pipeline {

    
   agent any
    
   environment{
    
     dockerRegistry="192.168.3.12:6007"
    
     //后端变量
    
     backPort=7001
    
     backServerPort=8080
    
     backName="rapid-demo-back"
    
     backGitUrl="https://gitee.com/lvlinguang/rapid-demo-back.git"
    
     backBranch="*/master"
    
     backDockerImage="${dockerRegistry}/library/${backName}:v2.1.2"
    
     //前端变量
    
     frontPort=7002
    
     frontServerPort=8080
    
     frontName="rapid-demo-web"
    
     frontGitUrl="https://gitee.com/lvlinguang/rapid-demo-web.git"
    
     frontBranch="*/master"
    
     frontDockerImage="${dockerRegistry}/library/${frontName}:v2.1.2"
    
   }
    
   parameters {
    
     booleanParam(name: 'ENABLE_BACKEND_BUILD', defaultValue: true, description: 'docker后端构建')
    
     booleanParam(name: 'ENABLE_FRONTEND_BUILD', defaultValue: true, description: 'docker前端构建')
    
     booleanParam(name: 'ENABLE_BACKEND_DEPLOY', defaultValue: true, description: 'docker后端部署')
    
     booleanParam(name: 'ENABLE_FRONTEND_DEPLOY', defaultValue: true, description: 'docker前端部署')
    
   }
    
   post {
    
       always {
    
       // 删除工作目录
    
       cleanWs()
    
       }
    
   }
    
   stages {
    
     stage('初始化') {
    
     steps {
    
         echo '初始化。。。'
    
     }
    
     }
    
     stage('后端打包'){
    
     when{
    
         expression{params.ENABLE_BACKEND_BUILD}
    
     }
    
     steps{
    
         script{
    
             // git代码拉取。。。
    
             gitClone(backGitUrl, backBranch);
    
             // mvn打包。。。
    
             sh "/usr/local/apache-maven-3.8.2/bin/mvn -U clean install -Dmaven.test.skip=true"
    
             // docker打包。。。
    
             sh "docker build -t ${backDockerImage} ."
    
             // harbor登录
    
             withCredentials([usernamePassword(credentialsId: '6a2f3d27-7641-4666-90e1-833c39afde8d', passwordVariable: 'password', usernameVariable: 'username')]) {
    
                 sh "docker login -u ${username} -p ${password} ${dockerRegistry}"
    
             }
    
             // docker推送至Harbor
    
             sh "docker push ${backDockerImage}"
    
             // 删除镜像
    
             sh "docker rmi ${backDockerImage}"
    
             echo '打包完成。。。'
    
         }
    
     }
    
     }
    
     //前端打包
    
     stage('前端打包'){
    
     when {
    
         expression { params.ENABLE_FRONTEND_BUILD }
    
     }
    
     steps {
    
         script {
    
             echo '前端打包。。。'
    
             // git代码拉取。。。
    
             gitClone(frontGitUrl, frontBranch);
    
             // nodejs的npm进行打包。。。
    
             nodejs('node-v12.20.0') {
    
                 sh 'npm install --registry=https://registry.npm.taobao.org'
    
                 sh 'npm run build'
    
             }
    
             // docker打包。。。
    
             sh "docker build -t ${frontDockerImage} ."
    
             // harbor登录
    
             withCredentials([usernamePassword(credentialsId: '6a2f3d27-7641-4666-90e1-833c39afde8d', passwordVariable: 'password', usernameVariable: 'username')]) {
    
                 sh "docker login -u ${username} -p ${password} ${dockerRegistry}"
    
             }
    
             // docker推送至Harbor
    
             sh "docker push ${frontDockerImage}"
    
             // 删除镜像
    
             sh "docker rmi ${frontDockerImage}"
    
             echo '打包完成。。。'
    
         }
    
     }
    
     }
    
     stage('测试') {
    
     steps {
    
         echo '测试。。。'
    
     }
    
     }
    
     stage('后端发布') {
    
     when{
    
         expression{params.ENABLE_BACKEND_DEPLOY}
    
     }
    
     steps {
    
         script {
    
             echo '后端发布。。。'
    
             // 远程服务器
    
             def sshServer = getRemoteServer('192.168.3.15')
    
             //remote发布
    
             sshCommand remote: sshServer, command: "/usr/local/java/deploy.sh $backName $backDockerImage $backPort $backServerPort"
    
         }
    
     }
    
     }
    
     stage('前端发布') {
    
     when{
    
         expression{params.ENABLE_FRONTEND_DEPLOY}
    
     }
    
     steps {
    
         script{
    
             echo '前端发布。。。'
    
             // 远程服务器
    
             def sshServer = getRemoteServer('192.168.3.15')
    
             //remote发布
    
             sshCommand remote: sshServer, command: "/usr/local/java/deploy.sh $frontName $frontDockerImage $frontPort $frontServerPort"
    
         }
    
     }
    
     }
    
   }
    
 }
    
  
    
 //获取远程服务器
    
 def getRemoteServer(String ip='192.168.3.15',String credentialsId='6c17706c-f96f-4711-91e4-719234985cba'){
    
     def remote = [:]
    
     remote.name = ip
    
     remote.host = ip
    
     remote.port = 22
    
     remote.allowAnyHosts = true
    
     withCredentials([usernamePassword(credentialsId: credentialsId, passwordVariable: 'password', usernameVariable: 'username')]) {
    
     remote.user = "${username}"
    
     remote.password = "${password}"
    
     }
    
     return remote
    
 }
    
  
    
 //git代码下载
    
 def gitClone(String gitUrl, String gitBranch, String credentialsId = 'abd46afe-05df-4de1-a683-edefb9319110') {
    
     checkout([
    
         $class           : 'GitSCM',
    
         branches         : [[name: gitBranch]],
    
         extensions       : [],
    
         userRemoteConfigs: [[
    
                                     credentialsId: credentialsId,
    
                                     url          : gitUrl
    
                             ]]
    
     ])
    
 }
    
    
    
    
    AI助手

deploy.sh

复制代码
 #接收外部参数,由jenkinsfile执行部署脚本时传递

    
 projectName=$1
    
 imageName=$2
    
 port=$3
    
 serverPort=$4
    
  
    
 echo "镜像名: $imageName"
    
 echo "项目名: $projectName"
    
  
    
 # 容器id
    
 containerId=$(docker ps -a | grep -w "${projectName}" | awk '{print $1}')
    
 if [ "$containerId" != "" ] ;
    
 then
    
   #停掉容器
    
   docker stop "$containerId"
    
   #删除容器
    
   docker rm "$containerId"
    
   echo "成功删除容器"
    
 fi
    
  
    
 # 镜像id
    
 imageId=$(docker images | grep -w "${projectName}" | awk '{print $3}')
    
 if [ "$imageId" != "" ] ;
    
 then
    
   #删除镜像
    
   docker rmi -f "$imageId"
    
   echo "成功删除镜像"
    
 fi
    
  
    
 # 镜像下载
    
 docker pull "$imageName"
    
  
    
 # 启动容器
    
 docker run -d --name "$projectName" --restart=always -p"${port}":"${serverPort}" $imageName
    
 echo "容器启动成功"
    
    
    
    
    AI助手

4. 发布测试

jenkins新建pipeline项目,使用上面的pipeline script。

发布测试:

服务器查看项目是否启动:

访问测试:192.168.3.15:7002。

5. 优化方案

Build with Parameters 选择docker部署,如下图。

6、Docker+SpringCloud 流水线构建

Jenkins+Docker+SpringCloud持续集成流程:

大致流程说明:

  1. 开发人员每天把代码提交到Gitlab代码仓库。
  2. Jenkins从Gitlab中拉取项目源码,编译并打成jar包,然后构建成Docker镜像,将镜像上传到Harbor私有仓库。
  3. Jenkins发送SSH远程命令,让生产部署服务器到Harbor私有仓库拉取镜像到本地,然后创建容器。
  4. 最后,用户可以访问到容器。

1. 服务器列表

虚拟机统采用CentOS7:

名称 主机名 安装的软件
代码托管服务器 gitlab-server gitlab-ce-14.6.0-ce.0.el7.x86_64.rpm
持续集成服务器 jenkins-server jdk-8u171-linux-x64.tar.gz、jenkins-2.319.1-1.1.noarch.rpm、apache-maven-3.8.4-bin.tar.gz、jdk-11.0.9_linux-x64_bin.tar.gz、git、PostgreSQL 12、sonarqube-8.9.6.50800.zip、docker-ce-18.06.3.ce、
Docker仓库服务器 docker-server docker-ce-18.06.3.ce、harbor-offline-installer-v2.4.1.tgz
生产部署服务器 production-server docker-ce-18.06.3.ce、nginx/1.20.2

2. Docker安装与配置

1)卸载旧版本

复制代码
 yum list installed | grep docker
    
 yum -y remove docker的包名称
    
    
    
    
    AI助手

删除docker的所有镜像和容器:

复制代码
    rm -rf /var/lib/docker
    
    AI助手

2)安装必要的软件包

复制代码
 yum install -y yum-utils \

    
 device-mapper-persistent-data \
    
 lvm2
    
    
    
    
    AI助手

3)设置下载的镜像仓库

复制代码
 yum-config-manager \

    
 --add-repo \
    
 https://download.docker.com/linux/centos/docker-ce.repo
    
    
    
    
    AI助手

4)列出有哪些docker版本

复制代码
 # yum list docker-ce --showduplicates | sort -r
    
 docker-ce.x86_64            18.06.3.ce-3.el7                    docker-ce-stable
    
 docker-ce.x86_64            18.06.2.ce-3.el7                    docker-ce-stable
    
 docker-ce.x86_64            18.06.1.ce-3.el7                    docker-ce-stable
    
 docker-ce.x86_64            18.06.0.ce-3.el7                    docker-ce-stable
    
 docker-ce.x86_64            18.03.1.ce-1.el7.centos             docker-ce-stable
    
    
    
    
    AI助手

5)安装指定版本(这里使用18.06.3.ce-3.el7版本)

复制代码
    yum install docker-ce-18.06.3.ce
    
    AI助手

6)查看版本

复制代码
 # docker -v

    
 Docker version 18.06.3-ce, build d7080c1
    
    
    
    
    AI助手

7)启动Docker

复制代码
 # systemctl start docker

    
 # systemctl enable docker
    
 # systemctl status docker
    
 ● docker.service - Docker Application Container Engine
    
    Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
    
    Active: active (running) 
    
    
    
    
    AI助手

8)添加阿里云镜像下载地址

复制代码
 # vim /etc/docker/daemon.json

    
 {
    
 	"registry-mirrors": ["https://zydiol88.mirror.aliyuncs.com"]
    
 }
    
    
    
    
    AI助手

9)重启Docker

复制代码
 # systemctl restart docker

    
 # systemctl status docker 
    
 ● docker.service - Docker Application Container Engine
    
    Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
    
    Active: active (running) 
    
    
    
    
    AI助手

3. 使用Dockerfile制作微服务镜像

下面演示利用Dockerfile制作一个Eureka注册中心的镜像。

1)上传 tensquare-eureka-server-1.0-SNAPSHOT.jar 包到 production-server 机器上

2)编写Dockerfile文件

复制代码
 # vim Dockerfile

    
 FROM openjdk:8-jdk-alpine
    
 ARG JAR_FILE
    
 COPY ${JAR_FILE} /target/tensquare-eureka-server-1.0-SNAPSHOT.jar
    
 EXPOSE 10086
    
 ENTRYPOINT ["java","-jar","/target/tensquare-eureka-server-1.0-SNAPSHOT.jar"]
    
    
    
    
    AI助手

3)构建镜像

复制代码
    docker build --build-arg JAR_FILE=tensquare-eureka-server-1.0-SNAPSHOT.jar -t tensquare-eureka-server:v1.0 .
    
    AI助手

4)查看镜像是否构建成功

复制代码
 # docker images

    
 REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
    
 tensquare-eureka-server   v1.0                70b977ee0289        35 seconds ago      150MB
    
    
    
    
    AI助手

5)创建容器

复制代码
    docker run -i --name=tensquare-eureka-server -p 10086:10086 tensquare-eureka-server:v1.0
    
    AI助手

浏览器访问容器:http://192.168.1.30:10086/

4. Harbor安装与配置

1)安装Docker并启动Docker(已完成)

2)安装docker-compose

复制代码
 sudo curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m)  \

    
 -o /usr/local/bin/docker-compose
    
    
    
    
    AI助手

3)给docker-compose添加执行权限

复制代码
    sudo chmod +x /usr/local/bin/docker-compose
    
    AI助手

4)查看docker-compose是否安装成功

复制代码
 # docker-compose -version

    
 docker-compose version 1.21.2, build a133471
    
    
    
    
    AI助手

5)下载Harbor压缩包并解压

下载地址:https://github.com/goharbor/harbor/releases

复制代码
 wget https://github.com/goharbor/harbor/releases/download/v2.4.1/harbor-offline-installer-v2.4.1.tgz

    
  
    
 tar -xzf harbor-offline-installer-v2.4.1.tgz -C /opt/install
    
    
    
    
    AI助手

6)修改Harbor的配置

复制代码
 cd /opt/install/harbor/

    
 cp harbor.yml.tmpl harbor.yml
    
  
    
 # vim harbor.yml
    
 hostname: 192.168.1.29
    
 port: 85
    
  
    
 --将https的都注释掉
    
 #https:
    
   # https port for harbor, default is 443
    
  # port: 443
    
   # The path of cert and key files for nginx
    
  # certificate: /your/certificate/path
    
  # private_key: /your/private/key/path
    
    
    
    
    AI助手

7)安装Harbor

复制代码
 ./prepare

    
 ./install.sh
    
    
    
    
    AI助手

8)启动Harbor

复制代码
 --启动。 ps. 如果下面的命令启动失败则用该命令 docker-compose -f /opt/install/harbor/harbor.yml up -d 启动

    
 docker-compose up -d
    
 --停止
    
 docker-compose stop
    
 --重启
    
 docker-compose restart
    
    
    
    
    AI助手

9)浏览器访问Harbor:http://192.168.1.29:85

默认账户admin,默认密码Harbor12345。

10)在Harbor创建用户和项目

(1)创建项目tensquare

Harbor的项目分为公开和私有的:

  • 公开项目:所有用户都可以访问,通常存放公共的镜像,默认有一个library公开项目。
  • 私有项目:只有授权用户才可以访问,通常存放项目本身的镜像。

(2)创建用户

harborZhangsan/harborZhang3

(3)给私有项目分配用户

角色 权限说明
访客 对于指定项目拥有只读权限
开发人员 对于指定项目拥有读写权限
维护人员 对于指定项目拥有读写权限,创建 Webhooks
项目管理员 除了读写权限,同时拥有用户管理/镜像扫描等管理权限

5. 镜像上传与下载

1)把镜像上传到Harbor

下面演示将production-server机器上的镜像上传到位于docker-server机器上的Harbor。

在production-server机器上完成下面步骤:

(1) 把Harbor地址加入到Docker信任列表

复制代码
 # vim /etc/docker/daemon.json

    
 {
    
     "registry-mirrors":[
    
     "https://zydiol88.mirror.aliyuncs.com"
    
     ],
    
     "insecure-registries":[
    
     "192.168.1.29:85" --这个是harbor地址
    
     ]
    
 }
    
    
    
    
    AI助手

(2)重启docker

复制代码
    systemctl restart docker
    
    AI助手

(3)登录Harbor

复制代码
    docker login -u harbor账号 -p harbor密码 192.168.1.29:85
    
    AI助手

(4)给需要上传到Harbor的镜像打标签

复制代码
 --查看当前机器有哪些镜像

    
 # docker images
    
 REPOSITORY                                              TAG                 IMAGE ID            CREATED             SIZE
    
 tensquare-eureka-server                                 v1.0                b82ccd24a80b        About an hour ago   150MB
    
  
    
 --给 tensquare-eureka-server 镜像打标签
    
 docker tag tensquare-eureka-server:v1.0 192.168.1.29:85/tensquare/tensquare-eureka-server:v1.0
    
    
    
    
    AI助手

(5)推送镜像到Harbor

复制代码
    docker push 192.168.1.29:85/tensquare/tensquare-eureka-server:v1.0
    
    AI助手

登录到Harbor便可以看到推送的镜像了:

2)从Harbor下载镜像

下面演示从Harbor上下载镜像。

在需要从Harbor上拉取镜像的机器需要完成如下步骤:

(1)安装Docker,并启动Docker

(2)把Harbor地址加入到Docker信任列表

复制代码
 # vim /etc/docker/daemon.json

    
 {
    
     "registry-mirrors":[
    
     "https://zydiol88.mirror.aliyuncs.com"
    
     ],
    
     "insecure-registries":[
    
     "192.168.1.29:85" --这个是harbor地址
    
     ]
    
 }
    
    
    
    
    AI助手

(3)重启docker

复制代码
    systemctl restart docker
    
    AI助手

(4)登录Harbor

复制代码
    docker login -u harbor账号 -p harbor密码 192.168.1.29:85
    
    AI助手

拉取镜像:

Harbor提供了镜像拉取命令。

复制代码
    docker pull 192.168.1.29:85/tensquare/tensquare-eureka-server@sha256:f8e52604958377d7934d3f211d0537df2fbd41a085e7f48673f963ce03b82a54
    
    AI助手

或者如下拉取:

复制代码
    docker pull 192.168.1.29:85/tensquare/tensquare-eureka-server:v1.0
    
    AI助手

6. Nginx安装与配置

安装必要环境:

复制代码
    yum -y install gcc gcc-c++ make libtool zlib zlib-devel openssl openssl-devel pcre pcre-devel
    
    AI助手

解压并安装:

复制代码
 tar -zxvf nginx-1.20.2.tar.gz -C /opt/install/

    
 cd /opt/install/nginx-1.20.2
    
 ./configure
    
 make
    
 make install
    
  
    
 --采用默认的配置安装,则启动命令在/usr/local/nginx/sbin
    
 # cd /usr/local/nginx/sbin
    
 # ./nginx -v
    
 nginx version: nginx/1.20.2
    
  
    
 --启动
    
 # ./nginx
    
 --停止
    
 # ./nginx -s stop
    
 --退出
    
 # ./nginx -s quit
    
 --重启
    
 # ./nginx -s reload
    
  
    
 --查看是否启动了
    
 # ps -ef | grep nginx
    
 root     11112     1  0 23:30 ?        00:00:00 nginx: master process ./nginx
    
 nobody   11113 11112  0 23:30 ?        00:00:00 nginx: worker process
    
 root     11115  8395  0 23:30 pts/0    00:00:00 grep --color=auto nginx
    
    
    
    
    AI助手

环境搭建OK了,下面将一个微服务项目部署到生产服务器上。

7. Jenkins配置

1)在Jenkins创建流水线项目tensquare-parent

2)指定Jenkinsfile脚本位置

3)添加项目参数project_name

4)添加分支参数

5)生成Harbor凭证脚本代码

复制代码
 withCredentials([usernamePassword(credentialsId: '0d458918-e2ea-4b51-8250-91c3731be288', passwordVariable: 'password', usernameVariable: 'username')]) {

    
     // some block
    
 }
    
    
    
    
    AI助手

6)在Jenkins上安装插件 Publish Over SSH

该插件,可以实现远程发送Shell命令。

7)配置远程部署服务器

拷贝 jenkins-server 机器上的公钥到远程部署服务器 production-server。

复制代码
 --在jenkins机器执行下面命令,将公钥复制到 192.168.1.30(production-server)

    
 ssh-copy-id 192.168.1.30
    
    
    
    
    AI助手

系统管理 -> 系统配置 -> Publish over SSH。

8)生成远程调用部署服务器的模板代码

复制代码
    sshPublisher(publishers: [sshPublisherDesc(configName: 'production-server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
    
    AI助手

9)添加一个port参数

8. 微服务项目配置

1)编写Jenkinsfile文件

一个微服务工程只需要一个Jenkinsfile文件。

复制代码
 //gitlab凭证ID

    
 def git_auth = "e4e02eb6-f6bb-4040-b842-c1423c397493"
    
 //gitlab的url地址
    
 def git_url = "git@gitlab.master.com:itheima_group/tensquare-parent.git"
    
 //镜像的版本号
    
 def tag = "latest"
    
 //Harbor的url地址
    
 def harbor_url = "reg.myharbor.com:85"
    
 //harbor镜像库项目名称
    
 def harbor_project_name = "tensquare"
    
 //Harbor的登录凭证ID
    
 def harbor_auth = "0d458918-e2ea-4b51-8250-91c3731be288"
    
  
    
 node {
    
     stage('拉取代码') {
    
     checkout([$class                             : 'GitSCM', branches: [[name: '*/${branch}']]
    
               , doGenerateSubmoduleConfigurations: false
    
               , extensions                       : []
    
               , submoduleCfg                     : []
    
               , userRemoteConfigs                : [
    
             [credentialsId: "${git_auth}", url: "${git_url}"]
    
     ]])
    
     }
    
     stage('代码审查') {
    
     //定义当前Jenkins的SonarQubeScanner工具
    
     def scannerHome = tool 'sonarqube-scanner'
    
     //引用当前JenkinsSonarQube环境
    
     withSonarQubeEnv('sonarqube-8.9.6.50800') {
    
         sh """
    
                      cd ${project_name}
    
                      ${scannerHome}/bin/sonar-scanner
    
              """
    
     }
    
     }
    
     stage('编译/构建镜像') {
    
     //定义镜像名称
    
     def imageName = "${project_name}:${tag}"
    
     //编译,安装公共工程
    
     sh "mvn -f tensquare-common clean install"
    
     //编译,构建本地镜像
    
     sh "mvn -f ${project_name} clean package dockerfile:build"
    
     //给镜像打标签
    
     sh "docker tag ${imageName} ${harbor_url}/${harbor_project_name}/${imageName}"
    
     //登录harbor并上传镜像
    
     withCredentials([
    
             usernamePassword(credentialsId: "${harbor_auth}"
    
                     , passwordVariable: 'password'
    
                     , usernameVariable: 'username')]) {
    
         //登录Harbor
    
         sh "docker login -u ${username} -p ${password} ${harbor_url}"
    
         //推送镜像到Harbor
    
         sh "docker push ${harbor_url}/${harbor_project_name}/${imageName}"
    
     }
    
  
    
     //上传完成后删除本地镜像
    
     sh "docker rmi -f ${imageName}"
    
     sh "docker rmi -f ${harbor_url}/${harbor_project_name}/${imageName}"
    
     }
    
     stage('部署服务') {
    
     sshPublisher(
    
             publishers: [
    
                     sshPublisherDesc(
    
                             configName: 'production-server'
    
                             , transfers: [
    
                             sshTransfer(
    
                                     cleanRemote: false
    
                                     , excludes: ''
    
                                     , execCommand: "/opt/jenkins_shell/tensquare/deploy.sh $harbor_url $harbor_project_name $project_name $tag $port"
    
                                     , execTimeout: 120000
    
                                     , flatten: false
    
                                     , makeEmptyDirs: false
    
                                     , noDefaultExcludes: false
    
                                     , patternSeparator: '[, ]+'
    
                                     , remoteDirectory: ''
    
                                     , remoteDirectorySDF: false
    
                                     , removePrefix: ''
    
                                     , sourceFiles: ''
    
                             )]
    
                             , usePromotionTimestamp: false
    
                             , useWorkspaceInPromotion: false
    
                             , verbose: false)
    
             ])
    
     }
    
 }
    
    
    
    
    AI助手

2)在部署服务器 production-server 机器上编写部署脚本

/opt/jenkins_shell/tensquare/deploy.sh:

复制代码
 #!/bin/sh

    
  
    
 #接收外部参数
    
 harbor_url=$1
    
 harbor_project_name=$2
    
 project_name=$3
    
 tag=$4
    
 port=$5
    
  
    
 imageName=$harbor_url/$harbor_project_name/$project_name:$tag
    
 echo "$imageName"
    
  
    
 #查询容器是否存在,存在则删除
    
 containerId=`docker ps -a | grep -w ${project_name}:${tag} | awk '{print $1}'`
    
 if [ "$containerId" != "" ] ; then
    
     #停掉容器
    
     docker stop $containerId
    
     #删除容器
    
     docker rm $containerId
    
     echo "成功删除容器"
    
 fi
    
  
    
 #查询镜像是否存在,存在则删除
    
 imageId=`docker images | grep -w $project_name | awk '{print $3}'`
    
 if [ "$imageId" != "" ] ; then
    
     #删除镜像
    
     docker rmi -f $imageId
    
     echo "成功删除镜像"JAR_FILE
    
 fi
    
  
    
 # 登录Harbor私服
    
 docker login -u harborZhangsan -p harborZhang3 $harbor_url
    
 # 下载镜像
    
 docker pull $imageName
    
 # 启动容器
    
 docker run -di -p $port:$port $imageName
    
 echo "容器启动成功"
    
    
    
    
    AI助手

给该脚本执行权限:

复制代码
    chmod +x deploy.sh
    
    AI助手

3)为每个微服务项目编写各自的sonar-project.properties文件

下面是微服务项目 tensquare-eureka-server 的一个例子,记得到Harbor上创建对应的项目。

复制代码
 # must be unique in a given SonarQube instance

    
 sonar.projectKey=tensquare-eureka-server
    
 # this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1.
    
 sonar.projectName=tensquare-eureka-server
    
 sonar.projectVersion=1.0
    
  
    
 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
    
 # This property is optional if sonar.modules is set.
    
 sonar.sources=.
    
 sonar.exclusions=**/test/**,**/target/**
    
 sonar.java.binaries=.
    
  
    
 sonar.java.source=1.8
    
 sonar.java.target=1.8
    
 #sonar.java.libraries=**/target/classes/**
    
  
    
 # Encoding of the source code. Default is default system encoding
    
 sonar.sourceEncoding=UTF-8
    
    
    
    
    AI助手

4)为每个微服务项目编写各自的Dockerfile文件

每个微服务项目的pom.xml添加如下插件:

复制代码
 <build>

    
     <plugins>
    
     <plugin>
    
         <groupId>org.springframework.boot</groupId>
    
         <artifactId>spring-boot-maven-plugin</artifactId>
    
     </plugin>
    
     <plugin>
    
         <groupId>com.spotify</groupId>
    
         <artifactId>dockerfile-maven-plugin</artifactId>
    
         <version>1.3.6</version>
    
         <configuration>
    
             <repository>${project.artifactId}</repository>
    
             <buildArgs>
    
                 <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
    
             </buildArgs>
    
         </configuration>
    
     </plugin>
    
     </plugins>
    
 </build>
    
    
    
    
    AI助手

tensquare-eureka-server 微服务的Dockerfile文件示例:

复制代码
 #FROM java:8

    
 FROM openjdk:8-jdk-alpine
    
 ARG JAR_FILE
    
 COPY ${JAR_FILE} app.jar
    
  
    
 EXPOSE 10086 --注意每个微服务项目的端口都不一样
    
  
    
 ENTRYPOINT ["java","-jar","/app.jar"]
    
    
    
    
    AI助手

十四、Ansible自动化部署流水线

1、Jenkins与Ansible集成

Jenkins与Ansible集成能让Jenkins执行ansible命令。

是具体步骤如下:

(1)安装Ansible插件:https://plugins.jenkins.io/ansible。

(2)在主控机器上安装 Ansible,并设置不进行 host key 检查。主控机器指的是真正执行ansible命令的机器,也就是Jenkins。我们需要在主控机器上自行安装Asible,然后修改主控机器的Ansible配置,不进行host key检查。

复制代码
 $cat /etc/ansible/ansible.cfg

    
  
    
 [defaults]
    
 host_key_checking = False
    
    
    
    
    AI助手

如果要求安全级别高,则应该提前将所有受控机器的fingerprint放到主控机器的know_hosts文件中。

(3)在Jenkins上进入Manage Jenkins→Global Tool Configuration→Ansible配置页面,配置Ansible的执行路径。我们可以同时添加多个Ansible版本。请留意Name字段的值,后面介绍的ansiblePlaybook步骤会使用到。

(4)在Jenkins上添加登录受控机器的凭证。Ansible与受控机器连接的凭证需要我们在Jenk-ins上手动添加。根据项目的实际情况,可以选择使用用户名和密码的方式或者用户名和密钥的方式登录。

(5)在pipeline中加入ansiblePlaybook步骤。

hosts文件内容如下:

复制代码
 [example]

    
 192.168.23.12
    
    
    
    
    AI助手

playbook.yml文件内容如下:

复制代码
 ---

    
 - hosts: example
    
   tasks:
    
     - debug: msg="{{ lookup('env','BUILD_TAG') }}"
    
 
    
    
    AI助手

Jenkinsfile的内容如下:

复制代码
 pipeline {

    
     agent any
    
  
    
     stages {
    
     stage('Deploy') {
    
         steps {
    
             ansiblePlaybook(
    
                 playbook: "${env.WORKSPACE}/playbook.yml", 
    
                 inventory: "${env.WORKSPACE}/hosts", 
    
                 credentialsId: 'vagrant'
    
                 )
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

ansiblePlaybook步骤执行的是ansible-playbook命令,其中playbook参数是playook文件的路径,inventory参数是inventory文件的路径,credentialsId参数就是在上一步中添加的凭证ID。

这样,Jenkins与Ansible的集成就算完成了。但是这只是刚刚开始,在实际工作中,我们还需要考虑自定义的公共role应该放在哪里等与Ansible相关的问题。

2、Ansible插件详解ansiblePlaybook步骤

ansiblePlaybook步骤除支持playbook、inventory、credetialsId三个参数外,还支持以下参数。

  • installation:字符串类型,值为前面设置的Name字段的值。此参数的作用不言自明,用于指定不同版本的Ansible。
  • vaultCredentialsId:Ansible vault 密码在 Jenkins 中的凭证 ID。它相当于 ansible 命令行的--vault-password-file参数。
  • disableHostKeyChecking:布尔类型,是否进行host key检查。
  • become:布尔类型,在执行操作时是否加上sudo。它相当于ansible命令行的--become参数。
  • becomeUser:字符串类型,切换到超级管理员用户名,默认是root。它相当于ansible命令行的--become-user参数。
  • limit:字符串类型,指定执行的主机。相当于ansible命令行的-l参数。多个主机之间使用逗号分隔。
  • tags:指定执行打上特定tag的任务。它相当于ansible命令行的-t参数。多个tag之间使用逗号分隔。
  • skippedTags:字符串类型,指定跳过哪些tag的任务。它相当于ansible命令行的--skip-tags参数。多个tag之间使用逗号分隔。
  • startAtTask:字符串类型,从指定任务开始执行。它相当于ansible命令行的--start-at-task参数。
  • forks:并行执行的进程数。相当于ansible命令行的-f参数。
  • extras:字符串类型,扩展参数。当ansiblePlaybook步骤的参数无法满足需求时,可以使用此参数。比如extras:'--syntax-check'。
  • extraVars:List<org.jenkinsci.plugins.ansible.ExtraVar>类型,扩展变量。它相当于ansible命令行的-e参数。使用它的方式比较特殊,格式如下:
复制代码
 extraVars: [

    
     <key>: '<value>',
    
     <key>: [value: '<value>', hidden: true|false]
    
 ]
    
    
    
    
    AI助手

extraVars支持hidden属性,当其值为true时,在执行日志中会隐藏参数值。

现在我们来看一下完整的代码示例:

复制代码
 pipeline {

    
     agent any 
    
     stages {
    
     stage('Syntax check ansible playbook') {
    
         steps {
    
             ansiblePlaybook(
    
                 disableHostKeyChecking: true,
    
                 playbook: "${env.wORKSPACE}/playbook.yml", 
    
                 inventory: "${env.WORKSPACE}/hosts", 
    
                 credentialsId: 'vagrant', 
    
                 extras: '---syntax-check'
    
             )
    
         }
    
     }
    
  
    
     stage('Deploy') {
    
         steps {
    
             ansiblePlaybook(                    
    
                 disableHostKeyChecking: true,
    
                 playbook: "${env.WORKSPACE}/playbook.yml", 
    
                 inventory: "${env.wORKSPACE}/hosts", 
    
                 credentialsId: 'vagrant', 
    
                 // skippedTags: 'debugtag', 
    
                 forks: 2,
    
                 limit:'example1,example', 
    
                 tags: 'debugtag,testtag', 
    
                 extraVars: [
    
                     login:'mylogin',
    
                     secret_key:[value:'g4dfKWENpeF6pY05',hidden:true]
    
                 ]
    
                 // startAtTask:'task4'
    
              )
    
         }
    
     }
    
     }
    
 }
    
    
    
    
    AI助手

最后执行的命令如下:

复制代码
    sshpass ******** ansible-playbook /app/jenkins/workspace/13.2.2-jenkins-integrate-with-ansible/playbook.yml -i /app/jenkins/workspace/13.2.2-jenkins-integrate-with-ansible/hosts -l example1, example -t debugtag,testtag -f 2 -u vagrant -k -e login=mylogin -e *******
    
    
    AI助手

ansiblePlaybook 步骤只是 Ansible 插件提供的两个步骤中的一个,还有 ansibleVault步骤。

放在配置文件中的MySQL连接密码,想必是不希望被所有人看见的。Ansible vault是Ansible的一个特性,它能帮助我们加解密配置文件或者某个配置项。

在ansiblePlaybook步骤中,vaultCredentialsId参数的作用就是,在ansible-playbook执行过程中,会对事先放在playbook中的密文进行解密,解密需要密码,vaultCredentialsId就是我们事先存储在Jenkins中的密码的凭证ID。

而ansibleVault步骤所做的事情就是执行Ansible提供的ansible-vault命令。该命令通常用于对敏感数据进行加解密。

ansibleVault支持以下参数:

  • action(必填):字符串类型,ansibleVault执行的操作类型。包括:
  • encrypt,加密文件。
  • encrypt_string,加密字符。
  • rekey,使用一个新的密码进行加密,但需要旧的密码。
  • decrypt,解密。
  • content:字符串类型,加密文本时的字符串内容。
  • input:字符串类型,追加到ansible-vault命令行后面的参数。
  • installation:字符串类型,与ansiblePlaybook步骤的installation参数的作用一样。
  • newVaultCredentialsId:字符串类型,使用新的凭证进行重新加密,相当于ansible-vault命令的--new-vault-password-file参数。
  • output:字符串类型,追加到ansible-vault命令行后面的参数,但是会放在input参数之前。对于此参数,无论是在插件源码还是在官方文档(没有任何说明文档)中,都看不出如何使用。
  • vaultCredentialsId(必填):字符串类型,密码的凭证ID。

接下来,我们看看ansibleVault应用场景的代码示例。

  • 对文本内容进行加密。
复制代码
 ansibleVault(

    
     action:"encrypt_string", 
    
     content:"${secret}",
    
     vaultCredentialsId:"vaultid", 
    
 )
    
    
    
    
    AI助手

对于content参数,通常通过参数化传入,而不是这样写死的。

  • 加密文件。
复制代码
 ansibleVault(

    
     action: "encrypt",
    
     vaultCredentialsId: "vaultid", 
    
     input: "./vault-test.yml"
    
 )
    
    
    
    
    AI助手
  • 更换vault密码。
复制代码
 ansibleVault(

    
     action:"rekey",
    
     vaultCredentialsId:"vaultid", 
    
     newVaultCredentialsId:"vaultid2", 
    
     input:"./vault-test.yml"
    
 )
    
    
    
    
    AI助手
  • 解密文件。
复制代码
 ansibleVault(

    
     action:"decrypt",
    
     vaultCredentialsId:"vaultid2", 
    
     input:"./vault-test.yml"
    
 )
    
    
    
    
    AI助手

由于上例中更换了vault密码,所以这里使用vaultid2进行解密。

十五、SonarQube流水线实现代码审查

SonarQube是一个用于管理代码质量的开放平台,可以快速定位代码中潜在的或者明显的错误。目前支持java、C#、C/C++、Python、PL/SQL、Cobol、JavaScrip、Groovy等二十几种编程语言的代码质量管理与检测。

1、SonarQube 平台搭建

1. 安装jdk11

sonarqube-8.9.6 要求必须是jdk11,不能是jdk8。

复制代码
 tar -zxf jdk-11.0.9_linux-x64_bin.tar.gz -C /opt/install/

    
 cd /opt/install/jdk-11.0.9
    
  
    
 # ./bin/java -version
    
 java version "11.0.9" 2020-10-20 LTS
    
 Java(TM) SE Runtime Environment 18.9 (build 11.0.9+7-LTS)
    
 Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.9+7-LTS, mixed mode)
    
    
    
    
    AI助手

2. 安装数据库PostgreSQL12

sonarqube-8.9.6 要求必须是PostgreSQL9.6及以上版本。

PostgreSQL官网:PostgreSQL: Linux downloads (Red Hat family)

安装PostgreSQL 12:

复制代码
 (1)安装存储库RPM文件

    
 sudo yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
    
  
    
 (2)安装安装PostgreSQL
    
 sudo yum install -y postgresql12-server
    
  
    
 (3)验证是否安装成功
    
 # rpm -aq | grep postgres
    
 postgresql12-libs-12.9-1PGDG.rhel7.x86_64
    
 postgresql12-12.9-1PGDG.rhel7.x86_64
    
 postgresql12-server-12.9-1PGDG.rhel7.x86_64
    
  
    
 (4)初始化数据库
    
 /usr/pgsql-12/bin/postgresql-12-setup initdb
    
  
    
 (5)设置开机自启动,并启动PostgreSQL服务
    
 # systemctl enable postgresql-12
    
 # systemctl start postgresql-12
    
 # systemctl status postgresql-12
    
 ● postgresql-12.service - PostgreSQL 12 database server
    
    Loaded: loaded (/usr/lib/systemd/system/postgresql-12.service; enabled; vendor preset: disabled)
    
    Active: active (running)
    
    
    
    
    AI助手

配置防火墙:

复制代码
 --查询5432端口是否开放

    
 # firewall-cmd --query-port=5432/tcp
    
 no
    
  
    
 --开放5432端口(5432为PostgreSQL默认端口)
    
 # firewall-cmd --zone=public --add-port=5432/tcp --permanent
    
 # firewall-cmd --reload
    
 # firewall-cmd --query-port=5432/tcp
    
 yes
    
    
    
    
    AI助手

初始化用户名和密码:

复制代码
 (1)切换到用户postgres,初始化postgresql数据库时默认自动创建postgres用户

    
 --postgresql数据库不能以root用户登录
    
 # su - postgres
    
 -bash-4.2$ 
    
  
    
 (2)登录数据库
    
 -bash-4.2$ psql -U postgres
    
 psql (12.9)
    
 Type "help" for help.
    
  
    
 postgres=# 
    
  
    
 (3)设置密码,设置postgres用户密码为postgres
    
 postgres=# ALTER USER postgres WITH PASSWORD 'postgres';
    
  
    
 (4)退出数据库
    
 postgres=# \q
    
 -bash-4.2$ exit
    
    
    
    
    AI助手

修改配置文件,配置远程访问:

复制代码
 cd /var/lib/pgsql/12/data

    
  
    
 # vim postgresql.conf
    
 listen_addresses = '*'
    
  
    
 # vim pg_hba.conf
    
 # IPv4 local connections:
    
 host    all             all             127.0.0.1/32            ident
    
 host    all             all             0.0.0.0/0               md5
    
 # IPv6 local connections:
    
 host    all             all             ::1/128                 ident
    
 host    all             all             0.0.0.0/0               md5
    
    
    
    
    AI助手

切换到root用户,重启数据库:

复制代码
 # systemctl restart postgresql-12

    
 # systemctl status postgresql-12 
    
 ● postgresql-12.service - PostgreSQL 12 database server
    
    Loaded: loaded (/usr/lib/systemd/system/postgresql-12.service; enabled; vendor preset: disabled)
    
    Active: active (running)
    
    
    
    
    AI助手

可以用Navicat Premium连接测试看是否可以进行远程连接了:

3. 安装SonarQube

1)配置基础环境

添加sonarqube系统用户:

SonarQube不能用root用户运行,所以需要添加一个非root用户。

复制代码
 # 创建用户组

    
 groupadd sonarqube
    
 # 创建用户
    
 useradd sonarqube -g sonarqube
    
 # 设置密码
    
 passwd sonarqube
    
    
    
    
    AI助手

默认的配置可能不满足要求,我们增大limits.conf文件里面的配置。

复制代码
 # vim /etc/security/limits.conf

    
 sonarqube soft nofile 131072
    
 sonarqube hard nofile 131072
    
 sonarqube soft nproc 8192
    
 sonarqube hard nproc 8192
    
    
    
    
    AI助手

默认的配置可能不满足要求,我们增大sysctl.conf文件里面的配置。

复制代码
 # vim /etc/sysctl.conf

    
 vm.max_map_count = 524288
    
 fs.file-max = 131072
    
  
    
 # sysctl  -p
    
    
    
    
    AI助手

在PostgreSQL中创建sonarqube数据库:

复制代码
 //切换到用户postgres

    
 # su - postgres
    
 -bash-4.2$ 
    
  
    
 //登录数据库
    
 -bash-4.2$ psql
    
 postgres=# 
    
  
    
 //创建用户sonar和密码sonar
    
 postgres=# create user sonar with password 'sonar';
    
  
    
 //创建数据库指定所属者
    
 postgres=# create database sonarqube owner sonar encoding='UTF8';
    
  
    
 //将数据库权限,全部赋给用户sonar
    
 postgres=# grant all on database sonarqube to sonar;
    
  
    
 //退出数据库
    
 postgres=# \q
    
 -bash-4.2$ exit
    
    
    
    
    AI助手

2)安装SonarQube

下载SonarQube并解压:

下载地址:Download | SonarQube

复制代码
 wget https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-8.9.6.50800.zip

    
  
    
 yum -y install unzip
    
 unzip sonarqube-8.9.6.50800.zip -d /opt/install/
    
    
    
    
    AI助手

创建持久数据文件和临时文件的路径,并授权sonarqube用户访问权限。

复制代码
 mkdir -p /opt/install/sonarqube-8.9.6.50800/data

    
 mkdir -p /opt/install/sonarqube-8.9.6.50800/temp
    
  
    
 chown -R sonarqube:sonarqube /opt/install/sonarqube-8.9.6.50800/data
    
 chown -R sonarqube:sonarqube /opt/install/sonarqube-8.9.6.50800/temp
    
  
    
 chown -R sonarqube:sonarqube /opt/install/sonarqube-8.9.6.50800/
    
    
    
    
    AI助手

修改sonar.properties核心配置文件:

复制代码
 cd /opt/install/sonarqube-8.9.6.50800/conf/

    
  
    
 # vim sonar.properties
    
 --sonarqube数据库的用户名与密码
    
 sonar.jdbc.username=sonar
    
 sonar.jdbc.password=sonar
    
 sonar.jdbc.url=jdbc:postgresql://192.168.1.25:5432/sonarqube
    
  
    
 sonar.web.javaOpts=-Xmx512m -Xms128m -XX:+HeapDumpOnOutOfMemoryError
    
 sonar.web.host=192.168.1.25
    
 sonar.web.context=/sonarqube
    
 sonar.web.port=9000
    
  
    
 sonar.path.data=/opt/install/sonarqube-8.9.6.50800/data
    
 sonar.path.temp=/opt/install/sonarqube-8.9.6.50800/temp
    
    
    
    
    AI助手

修改wrapper.conf核心配置文件:

复制代码
 # vim wrapper.conf

    
 --指定jdk11的路径
    
 wrapper.java.command=/opt/install/jdk-11.0.9/bin/java
    
    
    
    
    AI助手

SonarQube默认端口是9000,开放9000端口。

复制代码
 --查询9000端口是否开放

    
 # firewall-cmd --query-port=9000/tcp
    
 no
    
  
    
 --开放9000端口
    
 # firewall-cmd --zone=public --add-port=9000/tcp --permanent
    
 # firewall-cmd --reload
    
 # firewall-cmd --query-port=9000/tcp
    
 yes
    
    
    
    
    AI助手

切换到sonarqube用户启动程序:

复制代码
 su sonarqube

    
 cd /opt/install/sonarqube-8.9.6.50800/bin/linux-x86-64
    
 ./sonar.sh start
    
    
    
    
    AI助手

浏览器访问SonarQube:http://192.168.1.25:9000/sonarqube/

默认账号admin,默认密码admin。

更新密码,我的新密码adminsonar。

2、SonarQube实现代码审查

实现流程:

1. SonarQube上配置

关闭审查结果上传到SCM功能:

添加项目:



复制代码
    daae43d6a75f1367245e7d5acc79cb97f3c60bd8
    
    AI助手
复制代码
 mvn sonar:sonar \

    
   -Dsonar.projectKey=web-demo-pipeline-sonarqube \
    
   -Dsonar.host.url=http://192.168.1.25:9000/sonarqube \
    
   -Dsonar.login=daae43d6a75f1367245e7d5acc79cb97f3c60bd8
    
    
    
    
    AI助手

2. Jenkins上配置

1)安装插件 SonarQube Scanner

2)添加SonarQube凭证

提醒:不是一个sonarqube项目就需要在jenkins中添加一个sonarqube凭证,首次添加这个凭证后,后面在sonarqube创建的所有项目都可以共享这个凭证与jenkins交互。

3)配置SonarQube servers

Manage Jenkins(系统管理) -> Configure System(系统配置) -> SonarQube servers。

4)配置SonarQube Scanner

Manage Jenkins(系统管理) -> Global Tool Configuration(全局工具配置) -> SonarQube Scanner。

3. 项目上配置

在项目添加SonarQube代码审查。

1)非流水线项目

如果是非流水线项目按照本章步骤在项目中添加sonarqube审查代码。

在项目根目录下添加文件sonar-project.properties:

文件名必须是sonar-project.properties 。

必须放在项目根目录下,不能放在resources目录下。

复制代码
 # must be unique in a given SonarQube instance

    
 sonar.projectKey=web-demo-maven-sonarqube
    
 # this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1.
    
 sonar.projectName=web-demo-maven-sonarqube
    
 sonar.projectVersion=1.0
    
 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
    
 # This property is optional if sonar.modules is set.
    
 sonar.sources=.
    
 sonar.exclusions=**/test/**,**/target/**
    
 sonar.java.source=1.8
    
 sonar.java.target=1.8
    
 # Encoding of the source code. Default is default system encoding
    
 sonar.sourceEncoding=UTF-8
    
    
    
    
    AI助手

记得把修改提交到gitlab上。

2)流水线项目

如果是流水线项目按照本章步骤在项目中添加sonarqube审查代码。

项目根目录下添加文件sonar-project.properties:

文件名必须是sonar-project.properties 。

必须放在项目根目录下,不能放在resources目录下。

复制代码
 # must be unique in a given SonarQube instance

    
 sonar.projectKey=web-demo-pipeline-sonarqube
    
 # this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1.
    
 sonar.projectName=web-demo-pipeline-sonarqube
    
 sonar.projectVersion=1.0
    
 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
    
 # This property is optional if sonar.modules is set.
    
 sonar.sources=.
    
 sonar.exclusions=**/test/**,**/target/**
    
 sonar.java.source=1.8
    
 sonar.java.target=1.8
    
 # Encoding of the source code. Default is default system encoding
    
 sonar.sourceEncoding=UTF-8
    
    
    
    
    AI助手

在Jenkinsfile文件中加入SonarQube代码审查阶段:

复制代码
 pipeline {

    
     agent any
    
  
    
     stages {
    
     stage('拉取代码') {
    
         steps {
    
            checkout([$class: 'GitSCM', branches: [[name: '*/master']]
    
            , doGenerateSubmoduleConfigurations: false
    
            , extensions: [], submoduleCfg: []
    
            , userRemoteConfigs: [[credentialsId: 'e4e02eb6-f6bb-4040-b842-c1423c397493'
    
            , url: 'git@gitlab.master.com:it_group/web-demo-pipeline-sonarqube.git']]])
    
         }
    
     }
    
     stage('编译构建') {
    
         steps {
    
            sh label: '', script: 'mvn clean package'
    
         }
    
     }
    
     stage('SonarQube代码审查') {
    
         steps{
    
             script {
    
                 scannerHome = tool 'sonarqube-scanner'
    
             }
    
             withSonarQubeEnv('sonarqube-8.9.6.50800') {
    
              sh "${scannerHome}/bin/sonar-scanner"
    
             }
    
         }
    
     }
    
     stage('项目部署') {
    
         steps {
    
            deploy adapters: [tomcat9(credentialsId: '748ce750-b1af-4730-9e43-1b6b4905a8dc', path: '', url: 'http://tomcat.master.com:8080')], contextPath: null, war: 'target/*.war'
    
         }
    
     }
    
     }
    
  
    
     post {
    
       always {
    
     emailext body: '${FILE,path="src/main/resources/email.html"}',
    
         subject: '构建通知:${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS} !',
    
         to: 'xxxb@163.com,xxxx10@qq.com'
    
       }
    
     }
    
 }
    
    
    
    
    AI助手

记得把修改提交到gitlab上。

4. Jenkins上构建并到SonarQube查看审查结果

在Jenkins上构建后到SonarQube查看审查结果:

十六、Pipeline CICD 设计实战

1、设计pipeline的步骤

当团队开始设计第一个pipeline时,该如何下手呢?

以下是设计步骤:

第1步:了解网站的整体架构。这个过程就是了解系统是如何服务用户的。其间,还可以识别出哪些是关键系统。

第2步:找到服务之间、服务与组件之间、组件之间的依赖关系。

第3步:找到对外依赖最少的组件,将其构建、打包、制品管理自动化。

第4步:重复第3步,直到所有(不是绝对)的组件都使用制品库管理起来。

第5步:了解当前架构中所有的服务是如何从源码到最终部署上线的。

第6步:找出第一个相对不那么重要的服务,将在第5步中了解到的手动操作自动化。但是,通常不会直接照搬手动操作进行自动化,而是会进行一些改动,让pipeline更符合《持续交付》第5章中所介绍的“部署流水线相关实践”内容。

另外,之所以先从一个不那么重要的服务下手,是因为即使自动化脚本出现错误,也不至于让大家对自动化失去信心。这个过程也是让团队适应自动化的过程。

第7步:重复第6步,直到所有服务的所有阶段都自动化。这一步不是绝对的,也可以先自动化一部分服务,然后开始第8步。

第8步:加入自动化集成测试的阶段。
······

在现实项目中,远没有这么简单。而且,整个过程并不一定是顺序进行的,而是需要几个来回。
如果当前服务没有日志收集和监控,那么在第3步时就要开始准备了,免得后期返工。在第8步以后就要看团队的具体需求了。

2、Jenkins配置即代码

Jenkins用久了,会有一种莫名的紧张感。因为没有人清楚Jenkins都配置了什么,以至于最终没有人敢动它。但凡使用界面进行配置的,都会有这样的后果。

解决办法就是通过代码进行配置——Configuration as Code。

2018 年年初发布的一款 Configuration-as-Code 插件,实现了 Jenkins Configuration as Code (JCasC)。

通过JCasC插件,我们使用YAML文件来配置Jenkins。如此,我们就可以对配置进行版本化控制了。

YAML文件内容如下:

复制代码
 jenkins:

    
     agentProtocols:
    
     - "JNLP4-connect"
    
     disableRememberMe: false
    
     labelString: "master"
    
     mode: NORMAL 
    
     numExecutors: 2 
    
     primaryView:
    
     list:
    
         includeRegex: "b-.*"
    
         name: "b-project"
    
     quietPeriod: 5
    
     scmCheckoutRetryCount: 2 
    
     slaveAgentPort: 0
    
     systemMessage: "system descripte"
    
     views:
    
     - list:
    
     includeRegex: "a-.*"
    
     name: "a-project"
    
 unclassified:
    
     globalvaultconfiguration:
    
     configuration:
    
         vaultCredentialId: "vault-token"
    
         vaultUrl:"http://192.168.23.11:8200"
    
 tool:
    
     dockertool:
    
     installations:
    
     - home: "/home/vagrant"
    
       nam    e: "abc"
    
     git:
    
       installations:
    
       - home: "git"
    
     name: "Default"
    
    
    
    
    AI助手

虽然JCasC的设计非常棒,但是它还有很多插件需要进行适配,所以在生产环境下使用请谨慎。

那还有什么办法能对Jenkins的配置进行版本化呢?有一个不是办法的办法。

Jenkins在启动时,会执行$JENKINS_HOME目录下的init.groovy脚本,以及init.groovy.d下的所有Groovy文件。在这些Groovy脚本中,我们可以访问Jenkins实例,并对插件进行配置,从而实现版本化Jenkins的目标。

以下代码示例展示了如何在init.groovy中向Jenkins增加一个Maven配置。

复制代码
 import hudson.tools.*;

    
 import hudson.tasks.Maven.MavenInstaller;
    
 import hudson.tasks.Maven.MavenInstallation;
    
  
    
 // 取得Jenkins实例
    
 def instance = Jenkins.getInstance()
    
  
    
 def mavenVersion='3.5.2'
    
 // 拿到Maven插件在Jenkins中的实例
    
 def mavenTool = instance.getDescriptor("hudson.tasks.Maven")
    
 def mavenInstallations = mavenTool.getInstallations()
    
 def mavenInstaller = new MavenInstaller(mavenVersion)
    
 def installSourceProperty = new InstallSourceProperty([mavenInstaller])
    
 // 配置Maven插件
    
 def name= "jenkins-book-mvn-" + mavenVersion 
    
 def maven_inst = new MavenInstallation(
    
     name, // Name 
    
     "", // Home
    
     [installSourceProperty]
    
 )
    
 mavenInstallations += maven_inst
    
 mavenTool.setInstallations((MavenInstallation[]) mavenInstallations)
    
 mavenTool.save()//保存配置
    
    
    
    
    AI助手

理论上,Jenkins的所有配置都可以通过此方式进行设置。使用脚本命令行调试init.groovy

init.groovy脚本是在Jenkins启动时加载执行的,那是不是说,如果反复调试init.groovy脚本,就需要反复重启Jenkins?当然没有必要。

Jenkins 本身提供了一个特性:脚本命令行。

通过它,我们可以直接在界面上修改并执行Groovy脚本,而不需要重启Jenkins。

具体步骤如下:

单击Manage Jenkins→Script Console,在“Script Console”页面中,填入Groovy脚本,然后单击“Run”按钮执行。

脚本执行完成后,在命令框的下方输出日志。因为它是直接操作Jenkins实例的,脚本会立即生效,所以在生产环境下请谨慎使用。

3、Pipeline CICD 实战

ci.jenkinsfile:

复制代码
 #!groovy

    
  
    
 @Library('jenkinslibrary@master') _
    
  
    
 //func from shareibrary
    
 def build = new org.devops.build()
    
 def deploy = new org.devops.deploy()
    
 def tools = new org.devops.tools()
    
 def gitlab = new org.devops.gitlab()
    
 def toemail = new org.devops.toemail()
    
 def sonar = new org.devops.sonarqube()
    
 def sonarapi = new org.devops.sonarapi()
    
 def nexus = new org.devops.nexus()
    
 def artifactory = new org.devops.artifactory() 
    
  
    
  
    
 def runOpts
    
 //env
    
 String buildType = "${env.buildType}"
    
 String buildShell = "${env.buildShell}"
    
 String deployHosts = "${env.deployHosts}"
    
 String srcUrl = "${env.srcUrl}"
    
 String branchName = "${env.branchName}"
    
 String artifactUrl = "${env.artifactUrl}"
    
  
    
  
    
 if ("${runOpts}" == "GitlabPush"){
    
     branchName = branch - "refs/heads/"
    
     
    
     currentBuild.description = "Trigger by ${userName} ${branch}"
    
     gitlab.ChangeCommitStatus(projectId,commitSha,"running")
    
     env.runOpts = "GitlabPush"
    
  
    
     
    
 } else {
    
    userEmail = "2560350642@qq.com"
    
 }
    
  
    
  
    
 //pipeline
    
 pipeline{
    
     agent { node { label "build"}}
    
     
    
     
    
     stages{
    
  
    
     stage("CheckOut"){
    
         steps{
    
             script{
    
                
    
                 
    
                 println("${branchName}")
    
             
    
                 tools.PrintMes("获取代码","green")
    
                 checkout([$class: 'GitSCM', branches: [[name: "${branchName}"]], 
    
                                   doGenerateSubmoduleConfigurations: false, 
    
                                   extensions: [], 
    
                                   submoduleCfg: [], 
    
                                   userRemoteConfigs: [[credentialsId: 'gitlab-admin-user', url: "${srcUrl}"]]])
    
  
    
             }
    
         }
    
     }
    
     stage("Build"){
    
         steps{
    
             script{
    
             
    
                 tools.PrintMes("执行打包","green")
    
                 //build.Build(buildType,buildShell)
    
                 artifactory.main(buildType,buildShell)
    
                 artifactory.PushArtifact()
    
                 
    
                 //上传制品
    
                 //nexus.main("nexus")
    
                 
    
                 //发布制品
    
                 //sh " wget ${artifactUrl} && ls "
    
                 
    
                 
    
                 
    
  
    
  
    
                 //deploy.SaltDeploy("${deployHosts}","test.ping")
    
                 //deploy.AnsibleDeploy("${deployHosts}","-m ping ")
    
             }
    
         }
    
    }
    
    
    
    
    
     stage("QA"){
    
         steps {
    
             script{
    
                 tools.PrintMes("搜索项目","green")
    
                 result = sonarapi.SerarchProject("${JOB_NAME}")
    
                 println(result)
    
                 
    
                 if (result == "false"){
    
                     println("${JOB_NAME}---项目不存在,准备创建项目---> ${JOB_NAME}!")
    
                     sonarapi.CreateProject("${JOB_NAME}")
    
                 } else {
    
                     println("${JOB_NAME}---项目已存在!")
    
                 }
    
                 
    
                 tools.PrintMes("配置项目质量规则","green")
    
                 qpName="${JOB_NAME}".split("-")[0]   //Sonar%20way
    
                 sonarapi.ConfigQualityProfiles("${JOB_NAME}","java",qpName)
    
             
    
                 tools.PrintMes("配置质量阈","green")
    
                 sonarapi.ConfigQualityGates("${JOB_NAME}",qpName)
    
             
    
                 tools.PrintMes("代码扫描","green")
    
                 sonar.SonarScan("test","${JOB_NAME}","${JOB_NAME}","src")
    
                 
    
  
    
                 sleep 30
    
                 tools.PrintMes("获取扫描结果","green")
    
                 result = sonarapi.GetProjectStatus("${JOB_NAME}")
    
                 
    
                 
    
                 println(result)
    
                 if (result.toString() == "ERROR"){
    
                     toemail.Email("代码质量阈错误!请及时修复!",userEmail)
    
                     error " 代码质量阈错误!请及时修复!"
    
                     
    
                     
    
                 } else {
    
                     println(result)
    
                 }
    
             
    
             
    
  
    
             }
    
        }
    
    }
    
     }
    
     post {
    
     always{
    
         script{
    
             println("always")
    
         }
    
     }
    
     
    
     success{
    
         script{
    
             println("success")
    
             if ("${runOpts}" == "GitlabPush"){
    
                 gitlab.ChangeCommitStatus(projectId,commitSha,"success")
    
             }
    
             toemail.Email("流水线成功",userEmail)
    
         
    
         }
    
     
    
     }
    
     failure{
    
         script{
    
             println("failure")
    
             if ("${runOpts}" == "GitlabPush"){
    
                 gitlab.ChangeCommitStatus(projectId,commitSha,"failed")
    
             }
    
             toemail.Email("流水线失败了!",userEmail)
    
         }
    
     }
    
     
    
     aborted{
    
         script{
    
             println("aborted")
    
             if ("${runOpts}" == "GitlabPush"){
    
                 gitlab.ChangeCommitStatus(projectId,commitSha,"canceled")
    
             }
    
            toemail.Email("流水线被取消了!",userEmail)
    
         }
    
     
    
     }
    
     
    
     }
    
     
    
     
    
 }
    
    
    
    
    AI助手

deploy.jenkinsfile:

复制代码
 #!groovy

    
  
    
 @Library("jenkinslibrary@master") _ 
    
  
    
 String stackName = "${env.stackName}"
    
 String releaseVersion = "${env.releaseVersion}"
    
  
    
 def gitlab = new org.devops.gitlab()
    
 def k8s = new org.devops.kubernetes()
    
  
    
 pipeline{
    
    agent { node { label "build" }}
    
    
    
    stages{
    
    
    
    stage("Deploy"){
    
         steps{
    
             script{
    
             
    
                 //获取版本文件
    
                 stack = "${stackName}".toLowerCase()
    
                 response = gitlab.GetRepoFile(7,"demo-${stack}%2f${releaseVersion}-${stack}.yaml")
    
                 //发布应用
    
                 k8s.UpdateDeployment("demo-${stack}","demoapp",response)
    
  
    
             }  
    
        }
    
    }
    
    }
    
 }
    
    
    
    
    AI助手

/src/org/devops/build.groovy:

复制代码
 package org.devops

    
  
    
  
    
 //构建类型
    
 def Build(buildType,buildShell){
    
     def buildTools = ["mvn":"M2","ant":"ANT","gradle":"GRADLE","npm":"NPM"]
    
     
    
     
    
     println("当前选择的构建类型为 ${buildType}")
    
     buildHome= tool buildTools[buildType]
    
     
    
     if ("${buildType}" == "npm"){
    
     
    
     sh  """ 
    
         export NODE_HOME=${buildHome} 
    
         export PATH=\$NODE_HOME/bin:\$PATH 
    
         ${buildHome}/bin/${buildType} ${buildShell}"""
    
     } else {
    
     sh "${buildHome}/bin/${buildType}  ${buildShell}"
    
     }
    
 }
    
    
    
    
    AI助手

/src/org/devops/deploy.groovy:

复制代码
 package org.devops

    
  
    
  
    
 //saltstack
    
 def SaltDeploy(hosts,func){
    
     sh " salt \"${hosts}\" ${func} "
    
 }
    
  
    
  
    
 //ansible
    
  
    
 def AnsibleDeploy(hosts,func){
    
     sh " ansible ${func} ${hosts}"
    
     
    
     
    
 }
    
    
    
    
    AI助手

/src/org/devops/tools.groovy:

复制代码
 package org.devops

    
  
    
 //格式化输出
    
 def PrintMes(value,color){
    
     colors = ['red'   : "\033[40;31m >>>>>>>>>>>${value}<<<<<<<<<<< \033[0m",
    
           'blue'  : "\033[47;34m ${value} \033[0m",
    
           'green' : "[1;32m>>>>>>>>>>${value}>>>>>>>>>>[m",
    
           'green1' : "\033[40;32m >>>>>>>>>>>${value}<<<<<<<<<<< \033[0m" ]
    
     ansiColor('xterm') {
    
     println(colors[color])
    
     }
    
 }
    
    
    
    
    AI助手

/src/org/devops/gitlab.groovy:

复制代码
 package org.devops

    
  
    
 //封装HTTP请求
    
 def HttpReq(reqType,reqUrl,reqBody){
    
     def gitServer = "http://192.168.1.200:30088/api/v4"
    
     withCredentials([string(credentialsId: 'gitlab-token', variable: 'gitlabToken')]) {
    
       result = httpRequest customHeaders: [[maskValue: true, name: 'PRIVATE-TOKEN', value: "${gitlabToken}"]], 
    
             httpMode: reqType, 
    
             contentType: "APPLICATION_JSON",
    
             consoleLogResponseBody: true,
    
             ignoreSslErrors: true, 
    
             requestBody: reqBody,
    
             url: "${gitServer}/${reqUrl}"
    
             //quiet: true
    
     }
    
     return result
    
 }
    
  
    
  
    
 //更新文件内容
    
 def UpdateRepoFile(projectId,filePath,fileContent){
    
     apiUrl = "projects/${projectId}/repository/files/${filePath}"
    
     reqBody = """{"branch": "master","encoding":"base64", "content": "${fileContent}", "commit_message": "update a new file"}"""
    
     response = HttpReq('PUT',apiUrl,reqBody)
    
     println(response)
    
  
    
 }
    
  
    
 //获取文件内容
    
 def GetRepoFile(projectId,filePath){
    
     apiUrl = "projects/${projectId}/repository/files/${filePath}/raw?ref=master"
    
     response = HttpReq('GET',apiUrl,'')
    
     return response.content
    
 }
    
  
    
 //创建仓库文件
    
 def CreateRepoFile(projectId,filePath,fileContent){
    
     apiUrl = "projects/${projectId}/repository/files/${filePath}"
    
     reqBody = """{"branch": "master","encoding":"base64", "content": "${fileContent}", "commit_message": "create a new file"}"""
    
     response = HttpReq('POST',apiUrl,reqBody)
    
     println(response)
    
 }
    
  
    
  
    
 //更改提交状态
    
 def ChangeCommitStatus(projectId,commitSha,status){
    
     commitApi = "projects/${projectId}/statuses/${commitSha}?state=${status}"
    
     response = HttpReq('POST',commitApi,'')
    
     println(response)
    
     return response
    
 }
    
  
    
 //获取项目ID
    
 def GetProjectID(repoName='',projectName){
    
     projectApi = "projects?search=${projectName}"
    
     response = HttpReq('GET',projectApi,'')
    
     def result = readJSON text: """${response.content}"""
    
     
    
     for (repo in result){
    
    // println(repo['path_with_namespace'])
    
     if (repo['path'] == "${projectName}"){
    
         
    
         repoId = repo['id']
    
         println(repoId)
    
     }
    
     }
    
     return repoId
    
 }
    
  
    
 //删除分支
    
 def DeleteBranch(projectId,branchName){
    
     apiUrl = "/projects/${projectId}/repository/branches/${branchName}"
    
     response = HttpReq("DELETE",apiUrl,'').content
    
     println(response)
    
 }
    
  
    
 //创建分支
    
 def CreateBranch(projectId,refBranch,newBranch){
    
     try {
    
     branchApi = "projects/${projectId}/repository/branches?branch=${newBranch}&ref=${refBranch}"
    
     response = HttpReq("POST",branchApi,'').content
    
     branchInfo = readJSON text: """${response}"""
    
     } catch(e){
    
     println(e)
    
     }  //println(branchInfo)
    
 }
    
  
    
 //创建合并请求
    
 def CreateMr(projectId,sourceBranch,targetBranch,title,assigneeUser=""){
    
     try {
    
     def mrUrl = "projects/${projectId}/merge_requests"
    
     def reqBody = """{"source_branch":"${sourceBranch}", "target_branch": "${targetBranch}","title":"${title}","assignee_id":"${assigneeUser}"}"""
    
     response = HttpReq("POST",mrUrl,reqBody).content
    
     return response
    
     } catch(e){
    
     println(e)
    
     }
    
 }
    
  
    
 //搜索分支
    
 def SearchProjectBranches(projectId,searchKey){
    
     def branchUrl =  "projects/${projectId}/repository/branches?search=${searchKey}"
    
     response = HttpReq("GET",branchUrl,'').content
    
     def branchInfo = readJSON text: """${response}"""
    
     
    
     def branches = [:]
    
     branches[projectId] = []
    
     if(branchInfo.size() ==0){
    
     return branches
    
     } else {
    
     for (branch in branchInfo){
    
         //println(branch)
    
         branches[projectId] += ["branchName":branch["name"],
    
                                 "commitMes":branch["commit"]["message"],
    
                                 "commitId":branch["commit"]["id"],
    
                                 "merged": branch["merged"],
    
                                 "createTime": branch["commit"]["created_at"]]
    
     }
    
     return branches
    
     }
    
 }
    
  
    
 //允许合并
    
 def AcceptMr(projectId,mergeId){
    
     def apiUrl = "projects/${projectId}/merge_requests/${mergeId}/merge"
    
     HttpReq('PUT',apiUrl,'')
    
 }
    
    
    
    
    AI助手

/src/org/devops/toemail.groovy:

复制代码
 package org.devops

    
  
    
 //定义邮件内容
    
 def Email(status,emailUser){
    
     emailext body: """
    
         <!DOCTYPE html> 
    
         <html> 
    
         <head> 
    
         <meta charset="UTF-8"> 
    
         </head> 
    
         <body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0"> 
    
             <img src="http://192.168.1.200:8080/static/0eef74bf/images/headshot.png">
    
             <table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">   
    
                 <tr> 
    
                     <td><br /> 
    
                         <b><font color="#0B610B">构建信息</font></b> 
    
                     </td> 
    
                 </tr> 
    
                 <tr> 
    
                     <td> 
    
                         <ul> 
    
                             <li>项目名称:${JOB_NAME}</li>         
    
                             <li>构建编号:${BUILD_ID}</li> 
    
                             <li>构建状态: ${status} </li>                         
    
                             <li>项目地址:<a href="${BUILD_URL}">${BUILD_URL}</a></li>    
    
                             <li>构建日志:<a href="${BUILD_URL}console">${BUILD_URL}console</a></li> 
    
                         </ul> 
    
                     </td> 
    
                 </tr> 
    
                 <tr>  
    
             </table> 
    
         </body> 
    
         </html>  """,
    
         subject: "Jenkins-${JOB_NAME}项目构建信息 ",
    
         to: emailUser
    
     
    
 }
    
    
    
    
    AI助手

/src/org/devops/sonarqube.groovy:

复制代码
 package org.devops

    
  
    
  
    
 //scan
    
 def SonarScan(sonarServer,projectName,projectDesc,projectPath,branchName){
    
     
    
     //定义服务器列表
    
     def servers = ["test":"sonarqube-test","prod":"sonarqube-prod"]
    
     
    
     
    
     withSonarQubeEnv("${servers[sonarServer]}"){
    
     def scannerHome = "/home/jenkins/buildtools/sonar-scanner-3.2.0.1227-linux/"
    
     //def sonarServer = "http://192.168.1.200:9000"
    
     def sonarDate = sh  returnStdout: true, script: 'date  +%Y%m%d%H%M%S'
    
     sonarDate = sonarDate - "\n"
    
     
    
     
    
     sh """ 
    
         ${scannerHome}/bin/sonar-scanner -Dsonar.projectKey=${projectName} \
    
         -Dsonar.projectName=${projectName} -Dsonar.projectVersion=${sonarDate} -Dsonar.ws.timeout=30 \
    
         -Dsonar.projectDescription=${projectDesc} -Dsonar.links.homepage=http://www.baidu.com \
    
         -Dsonar.sources=${projectPath} -Dsonar.sourceEncoding=UTF-8 -Dsonar.java.binaries=target/classes \
    
         -Dsonar.java.test.binaries=target/test-classes -Dsonar.java.surefire.report=target/surefire-reports  -Dsonar.branch.name=${branchName} -X
    
   25.         """
    
     }
    
     
    
     //def qg = waitForQualityGate()
    
     //if (qg.status != 'OK') {
    
     //error "Pipeline aborted due to quality gate failure: ${qg.status}"
    
     //}
    
 }
    
    
    
    
    AI助手

/src/org/devops/sonarapi.groovy:

复制代码
 package org.devops

    
  
    
  
    
 //封装HTTP
    
  
    
 def HttpReq(reqType,reqUrl,reqBody){
    
     def sonarServer = "http://192.168.1.200:30090/api"
    
    
    
     result = httpRequest authentication: 'sonar-admin-user',
    
         httpMode: reqType, 
    
         contentType: "APPLICATION_JSON",
    
         consoleLogResponseBody: true,
    
         ignoreSslErrors: true, 
    
         requestBody: reqBody,
    
         url: "${sonarServer}/${reqUrl}"
    
         //quiet: true
    
     
    
     return result
    
 }
    
  
    
  
    
 //获取Sonar质量阈状态
    
 def GetProjectStatus(projectName){
    
     apiUrl = "project_branches/list?project=${projectName}"
    
     response = HttpReq("GET",apiUrl,'')
    
     
    
     response = readJSON text: """${response.content}"""
    
     result = response["branches"][0]["status"]["qualityGateStatus"]
    
     
    
     //println(response)
    
     
    
    return result
    
 }
    
  
    
 //搜索Sonar项目
    
 def SerarchProject(projectName){
    
     apiUrl = "projects/search?projects=${projectName}"
    
     response = HttpReq("GET",apiUrl,'')
    
  
    
     response = readJSON text: """${response.content}"""
    
     result = response["paging"]["total"]
    
  
    
     if(result.toString() == "0"){
    
    return "false"
    
     } else {
    
    return "true"
    
     }
    
 }
    
  
    
 //创建Sonar项目
    
 def CreateProject(projectName){
    
     apiUrl =  "projects/create?name=${projectName}&project=${projectName}"
    
     response = HttpReq("POST",apiUrl,'')
    
     println(response)
    
 }
    
  
    
 //配置项目质量规则
    
  
    
 def ConfigQualityProfiles(projectName,lang,qpname){
    
     apiUrl = "qualityprofiles/add_project?language=${lang}&project=${projectName}&qualityProfile=${qpname}"
    
     response = HttpReq("POST",apiUrl,'')
    
     println(response)
    
 }
    
  
    
  
    
 //获取质量阈ID
    
 def GetQualtyGateId(gateName){
    
     apiUrl= "qualitygates/show?name=${gateName}"
    
     response = HttpReq("GET",apiUrl,'')
    
     response = readJSON text: """${response.content}"""
    
     result = response["id"]
    
     
    
     return result
    
 }
    
  
    
 //配置项目质量阈
    
  
    
 def ConfigQualityGates(projectName,gateName){
    
     gateId = GetQualtyGateId(gateName)
    
     apiUrl = "qualitygates/select?gateId=${gateId}&projectKey=${projectName}"
    
     response = HttpReq("POST",apiUrl,'')
    
     println(response)println(response)
    
 }
    
    
    
    
    AI助手

/src/org/devops/nexus.groovy:

复制代码
 pakcage org.devops

    
  
    
  
    
 //获取POM中的坐标
    
 def GetGav(){
    
    //上传制品
    
     def jarName = sh returnStdout: true, script: "cd target;ls *.jar"
    
     env.jarName = jarName - "\n"
    
     
    
     def pom = readMavenPom file: 'pom.xml'
    
     env.pomVersion = "${pom.version}"
    
     env.pomArtifact = "${pom.artifactId}"
    
     env.pomPackaging = "${pom.packaging}"
    
     env.pomGroupId = "${pom.groupId}"
    
     
    
     println("${pomGroupId}-${pomArtifact}-${pomVersion}-${pomPackaging}")
    
  
    
     return ["${pomGroupId}","${pomArtifact}","${pomVersion}","${pomPackaging}"]
    
 }
    
  
    
  
    
 //Nexus plugin deploy
    
 def NexusUpload(){
    
     //use nexus plugin
    
     nexusArtifactUploader artifacts: [[artifactId: "${pomArtifact}", 
    
                                     classifier: '', 
    
                                     file: "${filePath}", 
    
                                     type: "${pomPackaging}"]], 
    
                         credentialsId: 'nexus-admin-user', 
    
                         groupId: "${pomGroupId}", 
    
                         nexusUrl: '192.168.1.200:30083', 
    
                         nexusVersion: 'nexus3', 
    
                         protocol: 'http', 
    
                         repository: "${repoName}", 
    
                         version: "${pomVersion}"
    
 }
    
  
    
 //mvn deploy
    
 def MavenUpload(){          
    
     def mvnHome = tool "M2"
    
     sh  """ 
    
     cd target/
    
     ${mvnHome}/bin/mvn deploy:deploy-file -Dmaven.test.skip=true  \
    
                             -Dfile=${jarName} -DgroupId=${pomGroupId} \
    
                             -DartifactId=${pomArtifact} -Dversion=${pomVersion}  \
    
                             -Dpackaging=${pomPackaging} -DrepositoryId=maven-hostd \
    
                             -Durl=http://192.168.1.200:30083/repository/maven-hostd 
    
     """
    
 }
    
  
    
 //制品晋级
    
 def ArtifactUpdate(updateType,artifactUrl){
    
  
    
     //晋级策略
    
     if ("${updateType}" == "snapshot -> release"){
    
     println("snapshot -> release")
    
  
    
     //下载原始制品
    
     sh "  rm -fr updates && mkdir updates && cd updates && wget ${artifactUrl} && ls -l "
    
  
    
     //获取artifactID 
    
     
    
     artifactUrl = artifactUrl -  "http://192.168.1.200:30083/repository/maven-hostd/"
    
     artifactUrl = artifactUrl.split("/").toList()
    
     
    
     println(artifactUrl.size())
    
     env.jarName = artifactUrl[-1] 
    
     env.pomVersion = artifactUrl[-2].replace("SNAPSHOT","RELEASE")
    
     env.pomArtifact = artifactUrl[-3]
    
     pomPackaging = artifactUrl[-1]
    
     pomPackaging = pomPackaging.split("\ .").toList()[-1]
    
     env.pomPackaging = pomPackaging[-1]
    
     env.pomGroupId = artifactUrl[0..-4].join(".")
    
     println("${pomGroupId}##${pomArtifact}##${pomVersion}##${pomPackaging}")
    
     env.newJarName = "${pomArtifact}-${pomVersion}.${pomPackaging}"
    
     
    
     //更改名称
    
     sh " cd updates && mv ${jarName} ${newJarName} "
    
     
    
     //上传制品
    
     env.repoName = "maven-release"
    
     env.filePath = "updates/${newJarName}"
    
     NexusUpload()
    
     }
    
 }
    
  
    
  
    
 def main(uploadType){
    
     GetGav()
    
     if ("${uploadType}" == "maven"){
    
     MavenUpload()
    
     } else if ("${uploadType}" == "nexus") {
    
     env.repoName = "maven-hostd"
    
     env.filePath = "target/${jarName}"
    
     NexusUpload()
    
     }
    
 }
    
    
    
    
    AI助手

/src/org/devops/artifactory.groovy:

复制代码
 package org.devops

    
  
    
  
    
 //Maven打包构建
    
 def MavenBuild(buildShell){
    
     def server = Artifactory.newServer url: "http://192.168.1.200:30082/artifactory"
    
     def rtMaven = Artifactory.newMavenBuild()
    
     def buildInfo
    
     server.connection.timeout = 300
    
     server.credentialsId = 'artifactory-admin-user' 
    
     //maven打包
    
     rtMaven.tool = 'M2' 
    
     buildInfo = Artifactory.newBuildInfo()
    
  
    
     String newBuildShell = "${buildShell}".toString()
    
     println(newBuildShell)
    
     rtMaven.run pom: 'pom.xml', goals: newBuildShell, buildInfo: buildInfo
    
     //上传build信息
    
     server.publishBuildInfo buildInfo
    
 }
    
  
    
  
    
  
    
 //上传制品
    
 def PushArtifact(){
    
     
    
     
    
     //重命名制品
    
     def jarName = sh returnStdout: true, script: "cd target;ls *.jar"
    
     jarName = jarName - "\n"
    
     def pom = readMavenPom file: 'pom.xml'
    
     env.pomVersion = "${pom.version}"
    
     env.serviceName = "${JOB_NAME}".split("_")[0]
    
     env.buildTag = "${BUILD_ID}"
    
     def newJarName = "${serviceName}-${pomVersion}-${buildTag}.jar"
    
     println("${jarName}  ------->>> ${newJarName}")
    
     sh " mv target/${jarName}  target/${newJarName}"
    
     
    
     //上传制品
    
     env.businessName = "${env.JOB_NAME}".split("-")[0]
    
     env.repoName = "${businessName}-${JOB_NAME.split("_")[-1].toLowerCase()}"
    
     println("本次制品将要上传到${repoName}仓库中!")   
    
     env.uploadDir = "${repoName}/${businessName}/${serviceName}/${pomVersion}"
    
     
    
     println('上传制品')
    
     rtUpload (
    
     serverId: "artifactory",
    
     spec:
    
         """{
    
         "files": [
    
             {
    
             "pattern": "target/${newJarName}",
    
             "target": "${uploadDir}/"
    
             }
    
         ]
    
         }"""
    
     )
    
 }
    
  
    
  
    
 def main(buildType,buildShell){
    
     if(buildType == "mvn"){
    
     MavenBuild(buildShell)
    
     }
    
 }
    
    
    
    
    AI助手

/src/org/devops/kubernetes.groovy:

复制代码
 package org.devops

    
  
    
  
    
 //封装HTTP请求
    
 def HttpReq(reqType,reqUrl,reqBody){
    
     def apiServer = "https://192.168.1.200:6443/apis/apps/v1"
    
     withCredentials([string(credentialsId: 'kubernetes-token', variable: 'kubernetestoken')]) {
    
       result = httpRequest customHeaders: [[maskValue: true, name: 'Authorization', value: "Bearer ${kubernetestoken}"],
    
                                        [maskValue: false, name: 'Content-Type', value: 'application/yaml'], 
    
                                        [maskValue: false, name: 'Accept', value: 'application/yaml']], 
    
             httpMode: reqType, 
    
             consoleLogResponseBody: true,
    
             ignoreSslErrors: true, 
    
             requestBody: reqBody,
    
             url: "${apiServer}/${reqUrl}"
    
             //quiet: true
    
     }
    
     return result
    
 }
    
 //新建Deployment
    
 def CreateDeployment(nameSpace,deployName,deplyBody){
    
     apiUrl = "namespaces/${nameSpace}/deployments/"
    
     response = HttpReq('POST',apiUrl,deplyBody)
    
     println(response)
    
 }
    
  
    
 //删除deployment
    
 def DeleteDeployment(nameSpace,deployName){
    
     apiUrl = "namespaces/${nameSpace}/deployments/${deployName}"
    
     response = HttpReq('DELETE',apiUrl,deplyBody)
    
     println(response)
    
 }
    
  
    
 //更新Deployment
    
 def UpdateDeployment(nameSpace,deployName,deplyBody){
    
     apiUrl = "namespaces/${nameSpace}/deployments/${deployName}"
    
     response = HttpReq('PUT',apiUrl,deplyBody)
    
     println(response)
    
 }
    
  
    
 //获取Deployment
    
 def GetDeployment(nameSpace,deployName){
    
     apiUrl = "namespaces/${nameSpace}/deployments/${deployName}"
    
     response = HttpReq('GET',apiUrl,'')
    
     return response
    
 }
    
    
    
    
    AI助手

全部评论 (0)

还没有任何评论哟~