由于公司内部项目众多,大量的项目使用同一套流程做CICD
- 那么势必会存在大量的重复代码
- 一旦某个公共的地方需要做调整,每个项目都需要修改

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

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

开发环境搭建

#下载安装包
链接:https://pan.baidu.com/s/1YRcBY3vFmn1FO37BeHiluQ
提取码:7hmu

#安装java
安装路径:D:\software\jdk
右键点击我的电脑 > 属性 > 高级系统设置, 新增环境变量:
JAVA_HOME D:\software\jdk
CLASSPATH .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;
PATH %JAVA_HOME%\bin #key(PATH)已存在,添加值即可

#安装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

安装时勾选如下选项:
image.png

library工作模式

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

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

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

在实际运行过程中,会把library中定义的groovy功能添加到构建目录中:
/var/jenkins_home/jobs/test-maven-build/branches/feature-CDN-2904.cm507o/builds/2/libs/my-shared-library/vars/devops.groovy

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

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

...
  stages {
    stage('build image') {
      steps {
         container('tools') {
           devops.buildImage("Dockerfile","192.168.136.10:5000/demo:latest")
         }
      }
    }
  }

  post {
    success {
      script {
          container('tools') {
              devops.notificationSuccess("dingTalk")
          }
      }
    }
  }
...

groovy项目新建

New Project
image.png

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

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

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

Groovy基本语法介绍

a)新建项目,名称为 jenkins-shared-library;
b)在项目src目录下新建package,名称为demo,在demo下新建groovy脚本,名为Hello;
c)在项目根目录下新建test目录,在test目录下新建groovy脚本,名为CrabDemo,并在其脚本中调用package(demo)中的函数;

总体结构如下:
image.png

#变量

// 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);

#方法

  • 调用本地方法 ```groovy def sum(int a, int b){ return a + b }

println(sum(1,2))


- 调用类中的方法
```groovy
# Hello.groovy
package demo

def sayHi(String content) {
    return ("hi, " + content)
}



# test/Crabdemo.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)
}


# test/Crabdemo.groovy
import demo.Hello

def demo() {
    new Hello().init("devops").sayHi().sayBye()
}

demo()
  • 捕获异常

    def exceptionDemo(){
      try {
          def val = 10 / 0
          println(val)
      }catch(Exception e) {
          println(e.toString())
          throw e
      }
    }
    exceptionDemo()
    
  • 计时器与循环 ```groovy 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 } } }


- 解析yaml文件

#org.yaml.snakeyaml.Yaml文件下载地址: [https://gitee.com/crabluo/jenkins-shared-library/tree/master/src](https://gitee.com/crabluo/jenkins-shared-library/tree/master/src)
```groovy
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()

library与Jenkins集成

以下梳理如何使用shared library实现最简单的helloworld输出功能的流程。

a)新建groovy项目(jenkins-shared-library)

  1. 在src目录下新建包,包名为com.crab.devops
  2. 在包下新建Hello.groovy脚本
  3. 在根目录下新建var目录,目录下新建devops.groovy脚本

结构如下:
image.png

Hello.groovy

package com.crab.devops

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
}

vars/devops.groovy

import com.crab.devops.Hello

def hello(String content) {
    return new Hello().hello(content)
}

b)在gitlab创建项目jenkins-shared-library,把代码推送到仓库

  1. gitlab上新建项目jenkins-shared-library
  2. 在本地项目根目录中新建 .gitignore 文件,文件内容见下文
  3. 根据页面提示在本地项目将代码推送到仓库

    image.png

.gitignore

.idea
out

代表git push的时候忽略.idea和out两个无用文件

c)配置Jenkins
[系统管理] -> [系统设置] -> [ Global Pipeline Libraries ]
- Library Name:crab-devops
- Default Version:master
- Modern SCM
- Source Code Management:Git

d)Jenkinsfile中引用
jenkins/pipelines/p11.yaml

@Library('crab-devops') _

pipeline {
    agent { label 'jnlp-slave'}

    stages {
        stage('hello-devops') {
            steps {
                script {
                    devops.hello("kazihuo").sayHi().answer().sayBye()
                }
            }
        } 
    }
    post {
        success { 
            echo 'Congratulations!'
        }
        failure {
            echo 'Oh no!'
        }
        always { 
            echo 'I will always say Hello again!'
        }
    }
}

说明:p11.yaml中第10行能直接调用devops是因为项目根目录下有var目录,目录下有devops.groovy脚本

e)结果
提交代码自动触发构建后,查看Blue Ocean,显示正常构建
image.png

library集成镜像构建及推送

先在jenkins上创建仓库登陆凭据,ID为 redential-registry ,为后续调用标识
image.png

/vars/devops.groovy

import com.crab.devops.Docker

/**
 *
 * @param repo, harbor.od.com/app/myblog
 * @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)
}

/srv/com.crab.devops/Docker.groovy

package com.crab.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, " + ignored.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
}

Jenkinsfile

@Library('crab-devops') _

pipeline {
    agent { label 'jnlp-slave'}
    options {
        timeout(time: 20, unit: 'MINUTES')
        gitLabConnection('gitlab')
    }
    environment {
        IMAGE_REPO = "harbor.od.com/app/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!'
        }
    }
}

丰富构建通知逻辑

此内容略,相关请参考对应笔记。

library集成k8s服务部署

具体步骤过于繁琐,故忽略,此处记载对应的实现流程!

库文件地址:https://gitee.com/crabluo/jenkins-shared-library.git

所有功能已经集成到jenkins-shared-library库中,主要是通过jenkinsfile调用库中的不同功能函数实现对应的功能实现。最重要的是deploy内容部分,此处以此进行说明。
在代码根目录下有目录deploy目录,目录下有yaml资源清单以模板形式存在,即Namespace,images,hostname等等指定内容以变量形式存在,在执行函数的过程中通过读取configmap中的信息而实现对应的内容替换,开发环境cm信息在下文已给出。替换完后apply应用资源清单,再通过代码逻辑实现pod是否正常运行的检测,当检测成功后继续执行下步骤。最后jenkins显示构建成功。

开发环境
$ cat devops-config-dev.txt
NAMESPACE=dev
INGRESS_MYBLOG=blog-dev.luffy.com
$ kubectl -n dev create configmap devops-config —from-env-file=devops-config-dev.txt