当前位置:   article > 正文

第六天 基于sharedLibrary进行CICD流程的优化_通过使用groovy实现jenkins的sharedl ibrary的开发

通过使用groovy实现jenkins的sharedl ibrary的开发

基于sharedLibrary进行CI/CD流程的优化

由于公司内部项目众多,大量的项目使用同一套流程做CICD

  • 那么势必会存在大量的重复代码
  • 一旦某个公共的地方需要做调整,每个项目都需要修改

因此本章主要通过使用groovy实现Jenkins的sharedLibrary的开发,以提取项目在CICD实践过程中的公共逻辑,提供一系列的流程的接口供公司内各项目调用。

开发完成后,对项目进行Jenkinsfile的改造,最后仅需通过简单的Jenkinsfile的配置,即可优雅的完成CICD流程的整个过程,此方式已在大型企业内部落地应用。

Library工作模式

由于流水线被组织中越来越多的项目所采用,常见的模式很可能会出现。 在多个项目之间共享流水线有助于减少冗余并保持代码 “DRY”。

流水线支持引用 “共享库” ,可以在外部源代码控制仓库中定义并加载到现有的流水线中。

@Library('my-shared-library') _
  • 1

在实际运行过程中,会把library中定义的groovy功能添加到构建目录中:

/var/jenkins_home/jobs/test-maven-build/branches/feature-CDN-2904.cm507o/builds/2/libs/my-shared-library/vars/devops.groovy
  • 1

使用library后,Jenkinsfile大致的样子如下:

@Library('my-shared-library') _

...
  stages {
    stage('build image') {
      steps {
         container('tools') {
           devops.buildImage("Dockerfile","172.21.51.67:5000/demo:latest")
         }
      }
    }
  }
  
  post {
    success {
      script {
          container('tools') {
              devops.notificationSuccess("dingTalk")
          }
      }
    }
  }
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
开发环境搭建

补录章节:Groovy及SpringBoot、SpringCloud都会使用

  • java
  • groovy
  • intelliJ idea
下载安装包

链接:https://pan.baidu.com/s/1B-bg2_IsB8dU7_62IEtnTg
提取码:wx6j

安装java

安装路径:D:\software\jdk

环境变量:

  • JAVA_HOME D:\software\jdk
  • CLASSPATH .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;
  • PATH %JAVA_HOME%\bin
安装groovy

解压路径:D:\software\groovy-3.0.2

环境变量:

  • GROOVY_PATH D:\software\groovy-3.0.2
  • PATH D:\software\groovy-3.0.2\bin
安装idea

安装路径:D:\software\IntelliJ IDEA 2019.2.3

新建项目测试

Library代码结构介绍

共享库的目录结构如下:

(root)
+- src                     # Groovy source files
|   +- org
|       +- foo
|           +- Bar.groovy  # for org.foo.Bar class
+- vars
|   +- foo.groovy          # for global 'foo' variable
|   +- foo.txt             # help for 'foo' variable
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

src 目录应该看起来像标准的 Java 源目录结构。当执行流水线时,该目录被添加到类路径下。

vars 目录定义可从流水线访问的全局变量的脚本。 每个 *.groovy 文件的基名应该是一个 Groovy (~ Java) 标识符, 通常是 camelCased

Groovy基本语法介绍

新建Groovy项目

  • 变量

    使用数据类型的本地语法,或者使用def关键字

    // Defining a variable in lowercase  
    int x = 5;
    
    // Defining a variable in uppercase  
    int X = 6; 
    
    // Defining a variable with the underscore in it's name 
    def _Name = "Joe"; 
    
    println(x); 
    println(X); 
    println(_Name); 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
  • 方法

    • 调用本地方法

      def sum(int a, int b){
          return a + b
      }
      
      println(sum(1,2))
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 调用类中的方法

      # Hello.groovy
      package demo
      
      def sayHi(String content) {
          return ("hi, " + content)
      }
      
      
      
      # Demo.groovy
      import demo.Hello
      
      def demo() {
          return new Hello().sayHi("devops")
      }
      println(demo())
      
      
      
      # 级联调用
      # Hello.groovy
      package demo
      
      def init(String content) {
          this.content = content
          return this
      }
      
      def sayHi() {
          println("hi, " + this.content)
          return this
      }
      
      def sayBye() {
          println("bye " + this.content)
      }
      
      
      # Demo.groovy
      import demo.Hello
      
      def demo() {
          new Hello().init("devops").sayHi().sayBye()
      }
      
      demo()
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
  • 异常捕获

    def exceptionDemo(){
        try {
            def val = 10 / 0
            println(val)
        }catch(Exception e) {
            println(e.toString())
            throw e
        }
    }
    exceptionDemo()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 计时器与循环

    import groovy.time.TimeCategory
    
    
    use( TimeCategory ) {
        def endTime = TimeCategory.plus(new Date(), TimeCategory.getSeconds(15))
        def counter = 0
        while(true) {
            println(counter++)
            sleep(1000)
            if (new Date() >= endTime) {
                println("done")
                break
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  • 解析yaml文件

    import org.yaml.snakeyaml.Yaml
    
    def readYaml(){
        def content = new File('myblog.yaml').text
        Yaml parser = new Yaml()
        def data = parser.load(content)
        def kind = data["kind"]
        def name = data["metadata"]["name"]
        println(kind)
        println(name)
    }
    readYaml()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
library与Jenkins集成

先来看一下如何使用shared library实现最简单的helloworld输出功能,来理清楚使用shared library的流程。

Hello.groovy
package com.luffy.devops

/**
* @author Yongxin
* @version v0.1
 */

/**
 * say hello
 * @param content
 */
def hello(String content) {
    this.content = content
    return this
}


def sayHi() {
    echo "Hi, ${this.content},how are you?"
    return this
}

def answer() {
    echo "${this.content}: fine, thank you, and you?"
    return this
}

def sayBye() {
    echo "i am fine too , ${this.content}, Bye!"
    return this
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

在gitlab创建项目,把library代码推送到镜像仓库。

配置Jenkins

[系统管理] -> [系统设置] -> [ Global Pipeline Libraries ]

  • Library Name:luffy-devops
  • Default Version:master
  • Source Code Management:Git
Jenkinsfile中引用

jenkins/pipelines/p11.yaml

@Library('luffy-devops') _

pipeline {
    agent { label 'jnlp-slave'}

    stages {
        stage('hello-devops') {
            steps {
                script {
                    devops.hello("树哥").sayHi().answer().sayBye()
                }
            }
        } 
    }
    post {
        success { 
            echo 'Congratulations!'
        }
        failure {
            echo 'Oh no!'
        }
        always { 
            echo 'I will always say Hello again!'
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

创建vars/devops.groovy

import com.luffy.devops.Hello

def hello(String content) {
    return new Hello().hello(content)
}
  • 1
  • 2
  • 3
  • 4
  • 5
library集成镜像构建及推送

需要实现的逻辑点:

  • docker build,docker push,docker login
  • 账户密码,jenkins凭据,(library中获取凭据内容),
  • docker login 172.21.51.67:5000
  • try catch
镜像构建逻辑实现

devops.groovy

/**
 *
 * @param repo, 172.21.51.67:5000/demo/myblog/xxx/
 * @param tag, v1.0
 * @param dockerfile
 * @param credentialsId
 * @param context
 */
def docker(String repo, String tag, String credentialsId, String dockerfile="Dockerfile", String context=".") {
    return new Docker().docker(repo, tag, credentialsId, dockerfile, context)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Docker.groovy

逻辑中需要注意的点:

  • 构建和推送镜像,需要登录仓库(需要认证)
  • 构建成功或者失败,需要将结果推给gitlab端
  • 为了将构建过程推送到钉钉消息中,需要将构建信息统一收集
package com.luffy.devops

/**
 *
 * @param repo
 * @param tag
 * @param credentialsId
 * @param dockerfile
 * @param context
 * @return
 */
def docker(String repo, String tag, String credentialsId, String dockerfile="Dockerfile", String context="."){
    this.repo = repo
    this.tag = tag
    this.dockerfile = dockerfile
    this.credentialsId = credentialsId
    this.context = context
    this.fullAddress = "${this.repo}:${this.tag}"
    this.isLoggedIn = false
    return this
}


/**
 * build image
 * @return
 */
def build() {
    this.login()
    retry(3) {
        try {
            sh "docker build ${this.context} -t ${this.fullAddress} -f ${this.dockerfile} "
        }catch (Exception exc) {
            throw exc
        }
        return this
    }
}


/**
 * push image
 * @return
 */
def push() {
    this.login()
    retry(3) {
        try {
            sh "docker push ${this.fullAddress}"
        }catch (Exception exc) {
            throw exc
        }
    }
    return this
}

/**
 * docker registry login
 * @return
 */
def login() {
    if(this.isLoggedIn || credentialsId == ""){
        return this
    }
    // docker login
    withCredentials([usernamePassword(credentialsId: this.credentialsId, usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
        def regs = this.getRegistry()
        retry(3) {
            try {
                sh "docker login ${regs} -u $USERNAME -p $PASSWORD"
            } catch (Exception exc) {
                echo "docker login err, " + exc.toString()
            }
        }
    }
    this.isLoggedIn = true;
    return this;
}

/**
 * get registry server
 * @return
 */
def getRegistry(){
    def sp = this.repo.split("/")
    if (sp.size() > 1) {
        return sp[0]
    }
    return this.repo
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90

Jenkinsfile

需要先在Jenkins端创建仓库登录凭据credential-registry

@Library('luffy-devops') _

pipeline {
    agent { label 'jnlp-slave'}
    options {
		timeout(time: 20, unit: 'MINUTES')
		gitLabConnection('gitlab')
	}
    environment {
        IMAGE_REPO = "172.21.51.67:5000/demo/myblog"
        IMAGE_CREDENTIAL = "credential-registry"
    }
    stages {
        stage('checkout') {
            steps {
                container('tools') {
                    checkout scm
                }
            }
        }
        stage('docker-image') {
            steps {
                container('tools') {
                    script{
                        devops.docker(
                            "${IMAGE_REPO}",
                            "${GIT_COMMIT}",
                            IMAGE_CREDENTIAL                          
                        ).build().push()
                    }
                }
            }
        }
    }
    post {
        success { 
            echo 'Congratulations!'
        }
        failure {
            echo 'Oh no!'
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
丰富构建通知逻辑

目前的构建镜像逻辑中缺少如下内容:

  • try逻辑中,若发生异常,是否该把异常抛出
    • 若直接抛出异常可能会导致多次重复的异常信息
    • 若不抛出,则如果未构建成功镜像,流水线感知不到错误
  • 通知gitlab端构建任务及状态
  • 构建通知格式

需要针对上述问题,做出优化

  1. 优化try逻辑

    def build() {
        this.login()
        def isSuccess = false
        def errMsg
        retry(3) {
            try {
                sh "docker build ${this.context} -t ${this.fullAddress} -f ${this.dockerfile}"
                isSuccess = true
            }catch (Exception err) {
                //ignore
                errMsg = err.toString()
            }
            // check if build success
            if(isSuccess){
                //todo
            }else {
                // throw exception,aborted pipeline
                error errMsg
            }
            return this
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
  2. 通知gitlab端构建任务及状态

    def build() {
        this.login()
        def isSuccess = false
        def errMsg = ""
        retry(3) {
            try {
                sh "docker build ${this.context} -t ${this.fullAddress} -f ${this.dockerfile} "
                isSuccess = true
            }catch (Exception err) {
                //ignore
                errMsg = err.toString()
            }
            // check if build success
            def stage = env.STAGE_NAME + '-build'
            if(isSuccess){
                updateGitlabCommitStatus(name: '${stage}', state: 'success')
            }else {
                updateGitlabCommitStatus(name: '${stage}', state: 'failed')
                // throw exception,aborted pipeline
                error errMsg
            }
    
            return this
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
  3. 钉钉消息通知格式

    由于每个stage都需要构建通知任务,因此抽成公共的逻辑,为各stage调用

    BuildMessage.groovy

    package com.luffy.devops
    
    def updateBuildMessage(String source, String add) {
        if(!source){
            source = ""
        }
        env.BUILD_TASKS = source + add + "\n                    \n                    "
        return env.BUILD_TASKS
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Docker.groovy 中调用

    def getObject(String repo, String tag, String credentialsId, String dockerfile="Dockerfile", String context="."){
    	...
        this.msg = new BuildMessage()
        return this
    }
    
    
    ...
    
    def build() {
    ...
            // check if build success
            def stage = env.STAGE_NAME + '-build'
            if(isSuccess){
                updateGitlabCommitStatus(name: '${stage}', state: 'success')
                this.msg.updateBuildMessage(env.BUILD_TASKS, "${stage} OK...  √")
            }else {
                updateGitlabCommitStatus(name: '${stage}', state: 'failed')
                this.msg.updateBuildMessage(env.BUILD_TASKS, "${stage} Failed...  x")
                // throw exception,aborted pipeline
                error errMsg
            }
    
            return this
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

使用Jenkinsfile来验证上述修改是否正确:

@Library('luffy-devops') _

pipeline {
    agent { label 'jnlp-slave'}
    options {
		timeout(time: 20, unit: 'MINUTES')
		gitLabConnection('gitlab')
	}
    environment {
        IMAGE_REPO = "172.21.51.67:5000/demo/myblog"
        IMAGE_CREDENTIAL = "credential-registry"
        DINGTALK_CREDS = credentials('dingTalk')
    }
    stages {
        stage('checkout') {
            steps {
                container('tools') {
                    checkout scm
                }
            }
        }
        stage('git-log') {
            steps {
                script{
                    sh "git log --oneline -n 1 > gitlog.file"
                    env.GIT_LOG = readFile("gitlog.file").trim()
                }
                sh 'printenv'
            }
        } 
        stage('build-image') {
            steps {
                container('tools') {
                    script{
                        devops.docker(
                            "${IMAGE_REPO}",
                            "${GIT_COMMIT}",
                            IMAGE_CREDENTIAL                          
                        ).build().push()
                    }
                }
            }
        }
    }
    post {
        success { 
            sh """
                curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
                    -H 'Content-Type: application/json' \
                    -d '{
                        "msgtype": "markdown",
                        "markdown": {
                            "title":"myblog",
                            "text": "
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/630548
推荐阅读
相关标签