Jenkins And DSL

Java基础

浏览数:57

2019-8-21

what is Jenkins

Jenkins Logo

Jenkins是一款由Java编写的开源的持续集成工具。

what is continuous integration

根据 ThoughtWorks 的 Martin Fowler 的观点,持续集成(continuous
integration)是一种软件开发实践,要求团队成员经常集成他们的工作。开发人员每次代码合并都会触发持续集成服务器进行自动构建,这个过程包括了编译、单元测试、集成测试、质量分析等步骤,通过自动化的过程进行验证,以尽快检测集成错误。这种方法会使得集成问题大幅减少,更快地实现有凝聚力的软件开发。

马丁福勒

业界普遍认同的持续集成的原则包括:

1)需要版本控制软件保障团队成员提交的代码不会导致集成失败。常用的版本控制软件有
Git、CVS、Subversion 等;

2)开发人员必须及时向版本控制库中提交代码,也必须经常性地从版本控制库中更新代码到本地;

3)需要有专门的集成服务器来执行集成构建。根据项目的具体实际,集成构建可以被软件的修改来直接触发,也可以定时启动,如每半个小时构建一次;

4)必须保证构建的成功。如果构建失败,修复构建过程中的错误是优先级最高的工作。一旦修复,需要手动启动一次构建。

流程图.png

Jenkins对持续集成的支持

Jenkins构建触发
Pipeline对部署的支持

what is DSL

Domain Specific Language 专门针对 一个特定的问题领域
含有建模所需的语法和语义,在与问题域相同的抽象层次对概念建模

DSL示例

pipeline

           stage('docker build') {
                steps {
                    //生成代码镜像1
                    script {
                        customImage = docker.build(docker_tag)
                    }
                }
            }
            stage('push image') {
                steps {
                    script {
                        docker.withRegistry("https://harbor.skyunion.net", 'harbor_skyunion_net') {
                            customImage.push()
                        }
                    }
                }
            }

            stage('deploy') {
                steps {
                    script {
                        deployK8s config.GROUP, config.PROJECT_NAME, IMAGE_TAG, params.TARGET, config.TOKEN, true
                    }
                }
            }

geb

import geb.Browser

Browser.drive {
    go "http://myapp.com/login"

    assert $("h1").text() == "Please Login"

    $("form.login").with {
        username = "admin"
        password = "password"
        login().click()
    }

    assert $("h1").text() == "Admin Section"
}

gradle

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

why Jenkins need pipeline

pipeline可以进入版本控制,以jenkinsfile的方式和源码放在一起


Jenkinsfile

比起shell,代码更强大,健壮,容易复用,可以以面向对象的方式进行开发、官方提供了大量与部署相关的方法、关键字

Jenkins-library

可以在发布过程中有更多控制手段

Input-demo

pipeline的基础概念

Stage:
一个Pipeline可以划分为若干个Stage,每个Stage代表一组操作。注意,Stage是一个逻辑分组的概念,可以跨多个Node。

Node:
一个Node就是一个Jenkins节点,或者是Master,或者是Agent,是执行Step的具体运行期环境。

Step:
Step是最基本的操作单元,小到创建一个目录,大到构建一个Docker镜像,由各类Jenkins
Plugin提供。

pipeline in Groovy

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                sh 'make'
            }
        }
        stage('Test'){
            steps {
                sh 'make check'
                junit 'reports/**/*.xml'
            }
        }
        stage('Deploy') {
            steps {
                sh 'make publish'
            }
        }
    }
}

DSL的种类

外部DSL(external DSL)
在主程序语言之外,用一种单独的语言表示领域专用语言,是从零开发的DSL,在词法分析、解析技术、解释、编译、代码生成等方面拥有独立的设施。开发外部DSL近似于从零开始实现一种拥有独特语法和语义的全新语言(markdown、xml、html)
内部DSL(internal DSL)
将现有的编程语言作为宿主语言,基于其设施建立专门面向特定领域的各种语义。(rails,gradle,pipeline)

Groovy适合用来构建DSL的原因

方法调用的语法

println('demo')
println 'demo'

运算符重载

操作符重载

强大的闭包,函数当作第一类对象

class PizzaShop {
    def config = [:]

    def static order(closure) {
        PizzaShop shop = new PizzaShop()
        shop.with closure
    }

    def size(theSize) { println "size is $theSize" }

    def toppings(String[] theToppings) { println "Toppings received $theToppings" }

    def methodMissing(name, args) {

    }
}

PizzaShop.order {
    size 'Large'
    toppings 'Olives', 'Bell Pepper', 'Onions'
}

强大的元编程能力

use(groovy.time.TimeCategory) {
    //直接用数字的写法
    println 1.minute.from.now //一分钟以后
    println 30.days.ago   //30天前的时间

    // 还可以与日期型的混用
    def someDate = new Date()
    println someDate - 3.months //三个月前的时间
}

Integer.metaClass.getDays { ->
    delegate
}

Integer.metaClass.getAgo { ->
    def today = Calendar.instance
    today.add(Calendar.DAY_OF_MONTH, -delegate)
    today
}

GregorianCalendar.metaClass.at { Double time ->
    def timeDbl = time.doubleValue()
    def hours=(int)timeDbl
    def minutes=(int)((timeDbl-hours)*100)
    delegate.set(Calendar.HOUR_OF_DAY,hours)
    delegate.set(Calendar.MINUTE,minutes)
    delegate.time
}

println 2.days.ago.at(4)

属性赋值的优雅

class Config {
    def map=[:]
    def methodMissing(String name,args){
        this.map[name]=args[0]
        println(args[0])
    }
    private String text;

    public String getMessage() {
        return "GET " + text;
    }

    public void setMessage(final String text) {
        this.text = "SET " + text
    }
}

config=new Config()
config.name "John"

println config.map['name']

def s2 = new Config(message: 'Groovy constructor')  // Named parameter in constructor.
assert 'GET SET Groovy constructor' == s2.getMessage()

def s3 = new Config()
s3.message = 'Groovy style'  // = assigment for property.
assert 'GET SET Groovy style' == s3.message  // get value with . notation.

可控的DSL沙盒

dsl = new File('./deploy.dsl')
def g = new GeneralBuildXml(xml)
def binding = new Binding()
binding.setProperty('exclude', new MethodClosure(g, 'exclude'))
binding.setProperty("out", new PrintWriter(new StringWriter()))
CompilerConfiguration conf = new CompilerConfiguration();
SecureASTCustomizer customizer = new SecureASTCustomizer();
customizer.with {
    closuresAllowed = true // 用户能写闭包
    methodDefinitionAllowed = true // 用户能定义方法
    importsWhitelist = [] // 白名单为空意味着不允许导入
    tokensWhitelist = [
            PLUS,
            EQUAL
    ].asImmutable()
    //将用户所能定义的常量类型限制为数值类型
    constantTypesClassesWhiteList = [
            String.class,
            Object.class,
    ].asImmutable()
}
customizer.setReceiversWhiteList(Arrays.asList(
        "java.lang.Object"
));
conf.addCompilationCustomizers(customizer);
new GroovyShell(binding, conf).evaluate(dsl)

超出沙盒允许的调用将会失败

exclude {
    dir "test"+"1", ".idea"
    file "composer.lock"
}
new File('demo').write('test')
java.lang.SecurityException: Method calls not allowed on [java.io.File]
    at org.codehaus.groovy.control.customizers.SecureASTCustomizer$SecuringCodeVisitor.visitMethodCallExpression(SecureASTCustomizer.java:924)
    at org.codehaus.groovy.ast.expr.MethodCallExpression.visit(MethodCallExpression.java:70)
    at org.codehaus.groovy.control.customizers.SecureASTCustomizer$SecuringCodeVisitor.visitExpressionStatement(SecureASTCustomizer.java:846)
    at org.codehaus.groovy.ast.stmt.ExpressionStatement.visit(ExpressionStatement.java:42)
    at org.codehaus.groovy.control.customizers.SecureASTCustomizer$SecuringCodeVisitor.visitBlockStatement(SecureASTCustomizer.java:806)
    at org.codehaus.groovy.ast.stmt.BlockStatement.visit(BlockStatement.java:71)
    at org.codehaus.groovy.control.customizers.SecureASTCustomizer.call(SecureASTCustomizer.java:616)
    at org.codehaus.groovy.control.CompilationUnit.applyToPrimaryClassNodes(CompilationUnit.java:1087)
    at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:631)
    at org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:609)
    at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:586)
    at groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:354)
    at groovy.lang.GroovyClassLoader.access$300(GroovyClassLoader.java:87)
    at groovy.lang.GroovyClassLoader$5.provide(GroovyClassLoader.java:323)
    at groovy.lang.GroovyClassLoader$5.provide(GroovyClassLoader.java:320)
    at org.codehaus.groovy.runtime.memoize.ConcurrentCommonCache.getAndPut(ConcurrentCommonCache.java:147)
    at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:318)
    at groovy.lang.GroovyShell.parseClass(GroovyShell.java:547)
    at groovy.lang.GroovyShell.parse(GroovyShell.java:559)
    at groovy.lang.GroovyShell.evaluate(GroovyShell.java:443)
    at groovy.lang.GroovyShell.evaluate(GroovyShell.java:491)
    at groovy.lang.GroovyShell$evaluate.call(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:116)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:128)
    at main.run(main.groovy:42)
    at groovy.lang.GroovyShell.runScriptOrMainOrTestOrRunnable(GroovyShell.java:264)
    at groovy.lang.GroovyShell.run(GroovyShell.java:377)
    at groovy.lang.GroovyShell.run(GroovyShell.java:366)
    at groovy.ui.GroovyMain.processOnce(GroovyMain.java:589)
    at groovy.ui.GroovyMain.run(GroovyMain.java:332)
    at groovy.ui.GroovyMain.access$1400(GroovyMain.java:69)
    at groovy.ui.GroovyMain$GroovyCommand.process(GroovyMain.java:291)
    at groovy.ui.GroovyMain.processArgs(GroovyMain.java:134)
    at groovy.ui.GroovyMain.main(GroovyMain.java:116)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.codehaus.groovy.tools.GroovyStarter.rootLoader(GroovyStarter.java:114)
    at org.codehaus.groovy.tools.GroovyStarter.main(GroovyStarter.java:136)

1 error

小结

Jenkins在引入pipeline之后变得更加强大和易于维护,也给我们一个启示,当我们在某一个领域经常需要解决重复性问题时,可以考虑实现一个
DSL 专门用来解决这些类似的问题。 而使用嵌入式 DSL
来解决这些问题是一个非常好的办法,我们并不需要重新实现解释器,也可以利用宿主语言的抽象能力。

作者:brightwang