zoukankan      html  css  js  c++  java
  • pipeline 使用之 Shared Libraries

    前言

    随着pipeline交付流水线在团队中的推广,使用pipeline脚本的job也迅速增加。虽然我们已经基于公司的技术栈特点做了一个尽可能通用的pipeline脚本样例,让搭建者只需要修改几个赋值参数就可以在自己的项目中应用,初衷是希望所有人能理解pipeline中的过程,但也发现一些比较麻烦的问题,比如有些人不熟悉具体的脚本拿来随意删改导致各种错误,还有就是我们在pipeline脚本中增加一些新功能时又需要通知所有的pipeline维护人员去修改,过程非常纠结。

    这时候就意味着我们需要用到pipline的共享库功能(Shared Libraries)了,在各种项目之间共享pipeline核心实现,以减少冗余并保证所有job在构建的时候会调用最新的共享库代码 。
    这篇我们就介绍下pipeline的这个黑科技:Shared Libraries
    目录结构
    Shared Library通过库名称、代码检索方法(如SCM)、代码版本三个要素进行定义,库名称尽量简洁,因为它会在脚本中被调用,在编写 Shared Library的时候,我们需要遵循固定的代码目录结构。
    Shared Library代码目录结构如下:

    image

    1. src目录就是标准的Java源目录结构。执行Pipeline时,该目录将添加到类路径中。
    2. vars目录托管定义可从Pipeline访问的全局脚本(一般我们可以在这里编写标准化脚本)。通常,每个.groovy文件的基本名称应使用驼峰(camelCased)模式,.txt(如果存在)可以包含格式化处理的文档。
    3. resources目录允许libraryResource从外部库中使用步骤来加载相关联的非Groovy文件。目前内部库不支持此功能。
    4. 定义全局库
    5. 这里只介绍全局 Shared Library的方式,通过Manage Jenkins » Configure System » Global Pipeline Libraries 的方式可以添加一个或多个共享库。
    6. 这些库将全局可用,系统中的任何Pipeline都可以利用这些库中实现的功能。并且通过配置SCM的方式,可以保证在每次构建时获取到指定Shared Library的最新代码
    7. 动态加载库
    8. 从2.7版本起,Pipeline: Shared Groovy Libraries plugin插件提供了一个新的参数“library”,用于在脚本中加载(non-implicit)库
      如果只需要加载全局变量/函数(从vars/目录中),语法非常简单:
      此后脚本中可以访问该库中的任何全局变量。
      library 'my-shared-library'

    采用此方式从src/目录中引用类也是可以的,不过只能动态地使用库类(无类型检查),从library步骤的返回值通过指定名称访问它们。比如static可以使用类似Java的语法来调用方法:

    library('my-shared-library').com.mycorp.pipeline.Utils.someStaticMethod()
    

    使用该library步骤时,您还可以指定一个版本,该指定版本将会覆盖默认版本。
    library 'my-shared-library@master'

    • Shared Libraries实战
    • 我们在https://testerhome.com/topics/10010已经介绍了一个项目基本样例,可以看到过程已经非常复杂(实际上我们后来还加了很多更复杂的功能),让普通业务工程师管理起来确实有点困难。
    • 通过参数化处理后,除了一些各项目的业务变量,整个过程在所有项目都是通用的,完全适合采用共享库的方式进行改造,屏蔽脚本的复杂度。
    • 在改造之前我们勾画了两种思路:pipeline模块库和模版库(姑且这么叫吧)。
    • 1.模块库方式
    • 模块库的方式,其实就是考虑把各个stage的实现通过函数化的方式抽象出来,比如获取代码的stage实现我们就抽象出codeFetch(),单元测试的 stage我们就抽象出unitTest().
    • 特点:业务工程师负责维护pipeline的初始赋值和整体结构,灵活度高,可自主裁剪stage场景
    • 不足:整体结构还是比较复杂,需要维护的共享脚本比较多,无法对交付流水线过程进行统一管理,Declarative Pipeline只支持script部分脚本的共享库。
    • pipeline代码样例:
    #!groovy
    library 'weiyi-pipeline-library'
    pipeline {
        agent any
        parameters {
        //repoBranch参数
        string(name:'repoBranch', defaultValue: 'master', description: 'git分支名称')
        //服务器选择
        choice(name: 'server',choices:'192.168.1.107,9090
    192.168.1.60,9090', description: '测试服务器列表选择(IP,JettyPort,Name,Passwd)')
        string(name:'dubboPort', defaultValue: '31100', description: '测试服务器的dubbo服务端口')
        //单元测试代码覆盖率要求,各项目视要求调整参数
        string(name:'lineCoverage', defaultValue: '20', description: '单元测试代码覆盖率要求(%),小于此值pipeline将会失败!')
        //若勾选在pipelie完成后会邮件通知测试人员进行验收
        booleanParam(name: 'isCommitQA',description: '是否在pipeline完成后,邮件通知测试人员进行人工验收',defaultValue: false )
        }
        //环境变量,初始确定后一般不需更改
        tools {
            maven 'maven3'
            jdk   'jdk8'
        }
        .......
        //pipeline的各个阶段场景
        stages {
            stage('代码获取') {
                steps {
                  codeFetch()
                }
            }
            stage('单元测试') {
                steps {
                  unitTest()
                }
            }
        }
    }
    
    共享库代码:
    // vars/codeFetch.groovy 
    def call() { echo "starting fetch code......" }
    

    2.模版库方式
    Declarative 1.2(released in late September, 2017),开始支持整条Declarative Pipeline作为共享库,使用条件如下:
    Only entire pipelines can be defined in shared libraries as of this time. This can only be done in vars/*.groovy, and only in a callmethod. Only one Declarative Pipeline can be executed in a single build, and if you attempt to execute a second one, your build will fail as a result.

    特点:可以将整条declarative pipeline作为共享库让各个项目调用,业务工程师只需要维护初始化赋值参数即可。
    不足:公司技术栈不统一的话,pipeline模版库的适配能力需要比较强(比如可能会出现虚拟机/docker共存,gradle/maven共存等多种情况),可能需要定义多个模版库,不过这些问题通过groovy代码逻辑上应该都可以控制。
    pipeline代码样例(敏感信息隐藏):

    #!groovy
    library 'weiyi-pipeline-library'
        def map = [:]
        /*参数化变量,运行时可选择*/
        //git分支名称
        map.put('repoBranch','master')
        //测试服务器列表选择(IP,JettyPort,Name,Passwd)
        map.put('server','192.168.1.107,9090
    192.168.1.60,9090')
        //测试服务器的dubbo服务端口
        map.put('dubboPort','31100')
        //单元测试代码覆盖率要求,各项目视要求调整参数
        map.put('lineCoverage','20')
    
        /*环境变量,初始确定后一般不需更改*/
        map.put('maven','maven3')
        map.put('jdk','jdk8')
    
        /*常量参数,初始确定后一般不需更改*/
        map.put("isDocker",false)
        //项目gitlab代码地址 
        map.put('REPO_URL','****')
        //git服务全系统只读账号,无需修改
        map.put('CRED_ID','****')
        //pom.xml的相对路径
        map.put('POM_PATH','pom.xml')
        //生成war包的相对路径
        map.put('WAR_PATH','rpc/war/target/*.war')
        //测试人员邮箱地址 
        map.put('QA_EMAIL','***')
        //接口测试job名称
        map.put('ITEST_JOBNAME','Guahao_InterfaceTest_ExpertPatient')
    pipelineCall("maven",map)
    
    
    共享库代码:
    #!groovy
    def call(String type,Map map) {
        if (type == "maven") {
        pipeline {
            agent any
            //参数化变量,目前只支持[booleanParam, choice, credentials, file, text, password, run, string]这几种参数类型,其他高级参数化类型还需等待社区支持
            parameters {
            //固定设置三类pipeline场景
            choice(name:'scene',choices:"scene1:完整流水线
    scene2:代码检查
    scene3:测试部署", description: '场景选择,默认运行完整流水线,如果只做开发自测可选择代码检查,如果只做环境部署可选择测试部署')
            //repoBranch参数后续替换成git parameter不再依赖手工输入,JENKINS-46451
            string(name:'repoBranch', defaultValue: "${map.repoBranch}", description: 'git分支名称')
            //服务器相关参数采用了组合方式,避免多次选择
            choice(name: 'server',choices:"${map.server}", description: '测试服务器列表选择')
            string(name:'dubboPort', defaultValue: "${map.dubboPort}", description: '测试服务器的dubbo服务端口')
            //单元测试代码覆盖率要求,各项目视要求调整参数
            string(name:'lineCoverage', defaultValue: "${map.lineCoverage}", description: '单元测试代码覆盖率要求(%),小于此值pipeline将会失败!')
            //若勾选在pipelie完成后会邮件通知测试人员进行验收
            booleanParam(name: 'isCommitQA', defaultValue: false, description: '是否在pipeline完成后,邮件通知测试人员进行人工验收')
            }
            //环境变量,初始确定后一般不需更改
            tools {
                maven "${map.maven}"
                jdk   "${map.jdk}"
            }
            //常量参数,初始确定后一般不需更改
            environment{
                REPO_URL="${map.REPO_URL}"
                //git服务全系统只读账号,无需修改
                CRED_ID="${map.CRED_ID}"
                //pom.xml的相对路径
                POM_PATH="${map.POM_PATH}"
                //生成war包的相对路径
                WAR_PATH="${map.WAR_PATH}"
                //测试人员邮箱地址
                QA_EMAIL="${map.QA_EMAIL}"
                //接口测试job名称
                ITEST_JOBNAME="${map.ITEST_JOBNAME}"
            }
    
            options {
                disableConcurrentBuilds()
                timeout(time: 1, unit: 'HOURS')
                //保持构建的最大个数
                buildDiscarder(logRotator(numToKeepStr: '10'))
            }
           post{
    
           }
            //pipeline的各个阶段场景
            stages {
                stage('代码获取') {
                    steps {
                    //一些初始化操作
                        script {
                        //根据param.server分割获取参数
                        def split=params.server.split(",")
                        serverIP=split[0]
                        jettyPort=split[1]
                        serverName=split[2]
                        serverPasswd=split[3]
                        //场景选择
                        println params.scene
                        //单元测试运行场景
                        isUT=params.scene.contains('scene1:完整流水线') || params.scene.contains('scene2:代码检查')
                        println "isUT="+isUT
                        //静态代码检查运行场景
                        isCA=params.scene.contains('scene1:完整流水线') || params.scene.contains('scene2:代码检查')
                        println "isCA="+isCA
                        //部署测试环境运行场景
                        isDP=params.scene.contains('scene1:完整流水线') || params.scene.contains('scene3:测试部署')
                        println "isDP="+isDP
                        //第三方库安全性检查
                        isDC=params.scene.contains('scene1:完整流水线')
                        println "isDC="+isDC
                        //接口测试运行场景
                        isIT=params.scene.contains('scene1:完整流水线')
                        println "isIT="+isIT
                        try{
                            wrap([$class: 'BuildUser']){
                            userEmail="${BUILD_USER_EMAIL},${QA_EMAIL}"
                            user="${BUILD_USER_ID}"
                            }
                       }catch(exc){
                            userEmail="${QA_EMAIL}"
                            user="system"
                        }
                      echo "starting fetchCode from ${REPO_URL}......"
                      // Get some code from a GitHub repository
                      git credentialsId:CRED_ID, url:REPO_URL, branch:params.repoBranch
                     }
                    }
                }
                stage('单元测试') {
                    when {
                         expression
                            {return isUT }
                    }
    
                }
    ................................................................................................以下省略几百行
     else if (type == "gradle"){
       pipeline {
            agent any
    ................................................................................................继续省略几百行
    }
    

    通过这种方式,整个pipeline脚本的实现和复杂度就被封装到Shared Library中。而且如果我要在原来的流水线基础上,新增一个stage的代码比如安全测试,或者需要对原有代码逻辑进行修改,只需要库开发人员修改共享库的功能,相关场景的项目在map赋值时增加相应的赋值参数即可。

    #!groovy
    pipeline {
        //在任何可用的代理上执行Pipeline
        agent any
        //参数化变量,目前只支持[booleanParam, choice, credentials, file, text, password, run, string]这几种参数类型,其他高级参数化类型还需等待社区支持。
        parameters {
        //git代码路径【参数值对外隐藏】
        string(name:'repoUrl', defaultValue: 'git@git.*****.com:*****/*****.git', description: 'git代码路径')
        //repoBranch参数后续替换成git parameter不再依赖手工输入,JENKINS-46451【git parameters目前还不支持pipeline】
        string(name:'repoBranch', defaultValue: 'master', description: 'git分支名称')
        //pom.xml的相对路径
        string(name:'pomPath', defaultValue: 'pom.xml', description: 'pom.xml的相对路径')
        //war包的相对路径
        string(name:'warLocation', defaultValue: 'rpc/war/target/*.war', description: 'war包的相对路径 ')
        //服务器参数采用了组合方式,避免多次选择,使用docker为更佳实践【参数值对外隐藏】
        choice(name: 'server',choices:'192.168.1.107,9090,*****,*****
    192.168.1.60,9090,*****,*****', description: '测试服务器列表选择(IP,JettyPort,Name,Passwd)')
        //测试服务器的dubbo服务端口
        string(name:'dubboPort', defaultValue: '31100', description: '测试服务器的dubbo服务端口')
        //单元测试代码覆盖率要求,各项目视要求调整参数
        string(name:'lineCoverage', defaultValue: '20', description: '单元测试代码覆盖率要求(%),小于此值pipeline将会失败!')
        //若勾选在pipelie完成后会邮件通知测试人员进行验收
        booleanParam(name: 'isCommitQA',description: '是否邮件通知测试人员进行人工验收',defaultValue: false )
        }
        //环境变量,初始确定后一般不需更改
        tools {
            maven 'maven3'
            jdk   'jdk8'
        }
        //常量参数,初始确定后一般不需更改
        environment{
            //git服务全系统只读账号cred_id【参数值对外隐藏】
            CRED_ID='*****-****-****-****-*********'
            //测试人员邮箱地址【参数值对外隐藏】
            QA_EMAIL='*****@*****.com'
            //接口测试(网络层)的job名,一般由测试人员编写
            ITEST_JOBNAME='Guahao_InterfaceTest_ExpertPatient'
        }
        options {
            //保持构建的最大个数
            buildDiscarder(logRotator(numToKeepStr: '10')) 
        }
        //定期检查开发代码更新,工作日每晚4点做daily build
        triggers {
            pollSCM('H 4 * * 1-5')
        }
       //pipeline运行结果通知给触发者
        post{
            success{
                script { 
                    wrap([$class: 'BuildUser']) {
                    mail to: "${BUILD_USER_EMAIL }",
                    subject: "PineLine '${JOB_NAME}' (${BUILD_NUMBER}) result",
                    body: "${BUILD_USER}'s pineline '${JOB_NAME}' (${BUILD_NUMBER}) run success
    请及时前往${env.BUILD_URL}进行查看"
                    }
                }
            }
            failure{
                script { 
                    wrap([$class: 'BuildUser']) {
                    mail to: "${BUILD_USER_EMAIL }",
                    subject: "PineLine '${JOB_NAME}' (${BUILD_NUMBER}) result",
                    body: "${BUILD_USER}'s pineline  '${JOB_NAME}' (${BUILD_NUMBER}) run failure
    请及时前往${env.BUILD_URL}进行查看"
                    }
                }
    
            }
            unstable{
                script { 
                    wrap([$class: 'BuildUser']) {
                    mail to: "${BUILD_USER_EMAIL }",
                    subject: "PineLine '${JOB_NAME}' (${BUILD_NUMBER})结果",
                    body: "${BUILD_USER}'s pineline '${JOB_NAME}' (${BUILD_NUMBER}) run unstable
    请及时前往${env.BUILD_URL}进行查看"
                    }
                }
            }
        }
    
        //pipeline的各个阶段场景
        stages {
            stage('代码获取') {
                steps {
                //根据param.server分割获取参数,包括IP,jettyPort,username,password
                script {
                    def split=params.server.split(",")
                    serverIP=split[0]
                    jettyPort=split[1]
                    serverName=split[2]
                    serverPasswd=split[3]
                }
                  echo "starting fetchCode from ${params.repoUrl}......"
                  // Get some code from a GitHub repository
                  git credentialsId:CRED_ID, url:params.repoUrl, branch:params.repoBranch
                }
            }
            stage('单元测试') {
                steps {
                  echo "starting unitTest......"
                  //注入jacoco插件配置,clean test执行单元测试代码. All tests should pass.
                  sh "mvn org.jacoco:jacoco-maven-plugin:prepare-agent -f ${params.pomPath} clean test -Dautoconfig.skip=true -Dmaven.test.skip=false -Dmaven.test.failure.ignore=true"
                  junit '**/target/surefire-reports/*.xml'
                  //配置单元测试覆盖率要求,未达到要求pipeline将会fail,code coverage.LineCoverage>20%.
                  jacoco changeBuildStatus: true, maximumLineCoverage:"${params.lineCoverage}"
                }
            }
            stage('静态检查') {
                steps {
                    echo "starting codeAnalyze with SonarQube......"
                    //sonar:sonar.QualityGate should pass
                    withSonarQubeEnv('SonarQube') {
                      //固定使用项目根目录${basedir}下的pom.xml进行代码检查
                      sh "mvn -f pom.xml clean compile sonar:sonar"
                    }
                    script {
                    timeout(10) { 
                        //利用sonar webhook功能通知pipeline代码检测结果,未通过质量阈,pipeline将会fail
                        def qg = waitForQualityGate() 
                            if (qg.status != 'OK') {
                                error "未通过Sonarqube的代码质量阈检查,请及时修改!failure: ${qg.status}"
                            }
                        }
                    }
                }
            }
    
            stage('部署测试环境') { 
                steps {
                    echo "starting deploy to ${serverIP}......"
                    //编译和打包
                    sh "mvn  -f ${params.pomPath} clean package -Dautoconfig.skip=true -Dmaven.test.skip=true"
                    archiveArtifacts warLocation
                    script {
                        wrap([$class: 'BuildUser']) {
                        //发布war包到指定服务器,虚拟机文件目录通过shell脚本初始化建立,所以目录是固定的
                        sh "sshpass -p ${serverPasswd} scp ${params.warLocation} ${serverName}@${serverIP}:htdocs/war"
                        //这里增加了一个小功能,在服务器上记录了基本部署信息,方便多人使用一套环境时问题排查,storge in {WORKSPACE}/deploy.log  & remoteServer:htdocs/war
                        Date date = new Date()
                        def deploylog="${date.toString()},${BUILD_USER} use pipeline  '${JOB_NAME}(${BUILD_NUMBER})' deploy branch ${params.repoBranch} to server ${serverIP}"
                        println deploylog
                        sh "echo ${deploylog} >>${WORKSPACE}/deploy.log"
                        sh "sshpass -p ${serverPasswd} scp ${WORKSPACE}/deploy.log ${serverName}@${serverIP}:htdocs/war"
                        //jetty restart,重启jetty
                        sh "sshpass -p ${serverPasswd} ssh ${serverName}@${serverIP} 'bin/jettyrestart.sh' "
                        }
                    }
                }
            }
    
          stage('接口自动化测试') {
                steps{
                    echo "starting interfaceTest......"
                    script {
                     //为确保jetty启动完成,加了一个判断,确保jetty服务器启动可以访问后再执行接口层测试。
                     timeout(5) {
                         waitUntil {
                            try {
                                //确保jetty服务的端口启动成功
                                sh "nc -z ${serverIP} ${jettyPort}"
                                //sh "wget -q http://${serverIP}:${jettyPort} -O /dev/null"
                                return true
                            } catch (exception) {
                                return false
                                }
                            }
                        }
                    //将参数IP和Port传入到接口测试的job,需要确保接口测试的job参数可注入
                     build job: ITEST_JOBNAME, parameters: [string(name: "dubbourl", value: "${serverIP}:${params.dubboPort}")]
                    }
                }
            }
    
            stage('UI自动化测试') { 
                 steps{
                 echo "starting UITest......"
                 //这个项目不需要UI层测试,UI自动化与接口测试的pipeline脚本类似
                 }
             }
    
            stage('性能自动化测试 ') { 
                steps{
                     echo "starting performanceTest......"
                    //视项目需要增加性能的冒烟测试,具体实现后续专文阐述
                    }
            }
    
            stage('通知人工验收'){
                steps{
                    script{
                        wrap([$class: 'BuildUser']) {
                        if(params.isCommitQA==false){
                            echo "不需要通知测试人员人工验收"
                        }else{
                            //邮件通知测试人员人工验收
                             mail to: "${QA_EMAIL}",
                             subject: "PineLine '${JOB_NAME}' (${BUILD_NUMBER})人工验收通知",
                             body: "${BUILD_USER}提交的PineLine '${JOB_NAME}' (${BUILD_NUMBER})进入人工验收环节
    请及时前往${env.BUILD_URL}进行测试验收"
                        }
    
                        }
                    }
                }
            }
    
            // stage('发布系统') { 
            //     steps{
            //         echo "starting deploy......"
            //     //    TODO发布环节后续专题阐述
            //     }
            // }
        }
    }
    

    当然,这个pipeline脚本只是针对该项目的场景展开并尽可能做到通用(在本公司大多数的项目还是适用的,只需要对参数化参数进行调整),部分项目会根据自己项目的情况进行裁剪和微调,比如一些耗时长的stage会使用parallel并发的方式进行等等

  • 相关阅读:
    深度学习原理与框架-Tensorflow卷积神经网络-神经网络mnist分类
    深度学习原理与框架-Tensorflow基本操作-mnist数据集的逻辑回归 1.tf.matmul(点乘操作) 2.tf.equal(对应位置是否相等) 3.tf.cast(将布尔类型转换为数值类型) 4.tf.argmax(返回最大值的索引) 5.tf.nn.softmax(计算softmax概率值) 6.tf.train.GradientDescentOptimizer(损失值梯度下降器)
    深度学习原理与框架-Tensorflow基本操作-实现线性拟合
    深度学习原理与框架-Tensorflow基本操作-变量常用操作 1.tf.random_normal(生成正态分布随机数) 2.tf.random_shuffle(进行洗牌操作) 3. tf.assign(赋值操作) 4.tf.convert_to_tensor(转换为tensor类型) 5.tf.add(相加操作) tf.divide(相乘操作) 6.tf.placeholder(输入数据占位
    深度学习原理与框架-Tensorflow基本操作-Tensorflow中的变量
    深度学习原理与框架-RNN网络框架-LSTM框架 1.控制门单元 2.遗忘门单元 3.记忆门单元 4.控制门单元更新 5.输出门单元 6.LSTM网络结构
    深度学习原理与框架-RNN网络架构-RNN网络 1.RNN的前向传播 2.RNN的反向传播
    深度学习原理与框架-卷积网络细节-三代物体检测算法 1.R-CNN 2.Fast R-CNN 3.Faster R-CNN
    线上服务内存OOM问题定位
    linux下追查线上问题常用命令
  • 原文地址:https://www.cnblogs.com/flyhgx/p/9181514.html
Copyright © 2011-2022 走看看