CI met docker, jenkins en pipeline scripts

Joost van Dieten

DeTesters BV

Inhoud

 

 

 

 

 

  • CI en CD
  • Jenkins
  • Docker
  • KlantCase Docker Jenkins
  • KlantCase Jenkins 2.0 multi-branch
  • Resultaten
  • Workshop

Continuous Integration

CI en CD

 

 

 

 

 

 

Jenkins

 

 

 

 

 

 

Docker

Docker containers wrap a piece of software in a complete filesystem that contains everything needed to run: code, runtime, system tools, system libraries – anything that can be installed on a server. This guarantees that the software will always run the same, regardless of its environment.

VMs vs Containers

KlantCase 1

​Jenkins omgeving onderhoud en opzetten voor veel teams is erg tijdrovend

  • 1 bouwtaak per onderdeel(build,test,deploy,etc) van de buildstraat dus erg veel UI configuratie
  • Ook veel bouwtaken van andere teams dus niet altijd overzicht
  • De tools (o.a. maven,java,cucumber,protractor) die nodig zijn voor het bouwen van de software moeten op elke server worden geconfigureerd.
  • Veel verschillende configuraties per team in Jenkins omgeving
  • Tussen buildtaken binnen een project veel verschil in configuratie
  • Overbelasting van de Jenkins server door te veel buildtaken
  • Een standaard buildtaak is slechts geschikt voor 1 specifieke branch

 

 

 

Docker jenkins opzet

physical

docker

Docker compose

Slave settings

KlantCase 2

​DevOps Teams bouwen nieuwe functionaliteit in een feature branch

  • Een feature branch per functie
  • In de MainLine is alle code van de applicatie aanwezig, dus alle features.
  • Niet geteste code kan vanuit de feature branch worden overgezet naar de Mainline 
  • Testomgeving voor testers is hierdoor onbruikbaar
  • Terugdraaien van code is tijdintensief langere doorlooptijd foutanalyse

 

 

 

Bouwtaken in Jenkins 1.0

Dekking branches

Oude Jenkins buildstraat monitort alleen develop

In DevOps omgeving moet alles te monitoren zijn dus ook de feature branches, bugfix branches etc.

Jenkins 2.0

Pipeline as code

Jenkinsfile

Minder UI configuraties

 

 

Multibranch pipelines

Met behulp van build scripts die de losse buildtaken vervangen worden alle branches meegenomen.

Multibranch pipelines

Pipeline script library

Scripts example

import groovy.json.*

version = '1.0'
gitCredentialsKey = "f8337c99-8c27-4dd7-99c0-2cd9db32f97d"
author = "Buildaccount ANV"
email = "Jenkins_Advies_Verzekeren@Rabobank.nl"
lastCommitHash = ""

/**
 * Function to do simple scm checkout 
 * Stash of the files is done with repo identifier user unstash 'repo' to make use of the checked out files on other nodes
 */
def checkout() {
    if (env.BRANCH_NAME == 'development'){
        checkout scm
        checkout([$class: 'GitSCM', branches: [[name: '*/development']], extensions: [[$class: 'CleanCheckout'],[$class: 'LocalBranch', localBranch: "development"]]])
    }else{
        checkout scm
    }
}

def buildStaticsBM(String staticSrcLocation){
    buildAndReleaseStatics(staticSrcLocation)
}

/**
 * Function to build java code. 
 * Precondtion for function usage: aiep_functions.checkout() has te be done first 
 *
 * @param branch the branch name to build
 *
 */
def buildJava() { 
	sh 'mvn clean deploy -U'
    if (env.BRANCH_NAME != 'development'){
        archive 'artifact/*.ear,container/is/properties/artifact/*.tar.gz'
        archive 'container/is/content/target/*-compressed.zip'
    }
    step([$class: 'JUnitResultArchiver', testResults: '**/target/surefire-reports/TEST-*.xml'])
}

/**
 * Function to test frontend with protractor tests
 */
def testProtractor(String staticSrcLocation){
    try{
        dir(staticSrcLocation){
            sh "npm install"
            sh "gulp e2e:bm"
        }
    }finally{
        publishHTML(target: [allowMissing: false, alwaysLinkToLastBuild: false, keepAll: false, reportDir: 'presentation/is/content/frontend/target/', reportFiles: 'e2e/htmlReport.html', reportName: 'Smoke test report'])
    }
}

public String release(){
    sshagent([gitCredentialsKey])
    {
        setGitUser()
        sh "mvn -B jgitflow:release-start -Prelease"
        sh "mvn -B jgitflow:release-finish -Prelease"
    }
}

def releaseHotfix(String branch) {
    prepareRelease()
    String releaseVersion = getPomVersion()
    String artifactId = getPomArtifact()
    sh "mvn clean deploy -Prelease"
    sshagent([gitCredentialsKey]) {
        setGitUser()
        sh "git commit -am 'Release version'"
        sh "git tag -m 'Release' -a ${artifactId}-${releaseVersion}"
        sh "git push origin ${artifactId}-${releaseVersion}"
        prepareSnapshot()
        sh "git commit -am 'Update to next snapshot'"
        sh "git push origin "+branch
    }
    return releaseVersion
}

def prepareRelease(){
	String version = getPomVersion().replace("-SNAPSHOT", "")
    sh "mvn versions:set -DnewVersion="+version+" -Prelease"
    sh "mvn versions:commit -Prelease"
}

def nextVersion(String version) {
    String[] v = version.replace("-SNAPSHOT", "").split("\\."); 
    version = "";
    for (int i = 0; i < v.length - 1; i++) {
        version += v[i]  + ".";
    }
    version += (v[v.length - 1].toInteger() + 1) + "";
    return version+"-SNAPSHOT";
}

def prepareSnapshot(){
	String version = nextVersion(getPomVersion())
    sh "mvn versions:set -DnewVersion="+version+" -Prelease"
    sh "mvn versions:commit -Prelease"
}

/**
 * Release function to release IS application
 *
 * @param repositoryURL the git ssh location of the repo
 * @param branch the branch name to build
 */
def releaseWithMavenPlugin(String repositoryURL, String branch){
    if (branch == 'development'){
        checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: '*/master'], [name: '*/development']], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'PreBuildMerge', options: [fastForwardMode: 'FF', mergeRemote: 'origin', mergeStrategy: 'DEFAULT', mergeTarget: 'master']], [$class: 'LocalBranch', localBranch: 'candidate'], [$class: 'WipeWorkspace'], [$class: 'CleanBeforeCheckout']], submoduleCfg: [], userRemoteConfigs: [[credentialsId: gitCredentialsKey, url: repositoryURL]]]
        return doReleaseWithMavenPlugin()
    }else{
        echo "Branch ${branch} did not match release criteria so skipping step.."
        return ""
    }
}

private String doReleaseWithMavenPlugin() {
    prepareRelease()
    String releaseVersion = getPomVersion()
    String artifactId = getPomArtifact()
    sh "mvn clean deploy -Prelease"
    sshagent([gitCredentialsKey]) {
        setGitUser()
        sh "git commit -am 'Release version'"
        sh "git tag -m 'Release' -a ${artifactId}-${releaseVersion}"
        sh "git push origin ${artifactId}-${releaseVersion}"
        sh "git push origin candidate:master"
        prepareSnapshot()
        sh "git commit -am 'Update to next snapshot'"
        sh "git pull origin development"
        sh "git push origin candidate:development"
    }
    return releaseVersion
}


/**
 * Function to build java code VIRP / CIRP lb and IS
 * Precondtion for function usage: aiep_functions.checkout() has te be done first 
 *
 * @param branch the branch name to build
 * @param channelCode is or lb
 */
def buildContainerJava(String branch, String channelCode) {   
    if (branch == 'development'){
        if (channelCode == "is"){
            sh 'mvn clean deploy -U'
        }else if (channelCode == "lb"){
            sh 'mvn clean deploy -Plocalbank'
        }else{
            echo "channelCode ${channelCode} not valid"
            return false
        }
    }else {
        if (channelCode == "is"){
            sh 'mvn clean install -U'
            archive 'artifact/*.ear, stub/pifcirpst/stub-properties/target/artifact/*.tar.gz, presentation/is/artifact/*.tar.gz'
        }else if (channelCode == "lb"){
            sh 'mvn clean install -Plocalbank'
        }else{
            echo "channelCode ${channelCode} not valid"
            return false
        }
    }
    step([$class: 'JUnitResultArchiver', testResults: '**/target/surefire-reports/TEST-*.xml'])
    return true
}

/**
 * Function to build statics for virp/cirp...
 * 
 * @param branch the branch name to build
 * 
 * @TODO merge this methode with buildStaticsBM
 *
 */
def buildStatics(String branch){
    if (branch == 'development'){
        sh 'mvn deploy -Pstatics -U'
        return true        
    } else{
        sh 'mvn clean install -Pstatics -U'
        archive 'presentation/is/content/target/*-compressed.zip'
        return false
    }
}

def buildAndReleaseStatics(String staticSrcLocation){
	buildAndReleaseStatics(staticSrcLocation, true)
}

def buildAndReleaseStatics(String staticSrcLocation, boolean bm){
    if (changesInPath(staticSrcLocation)) {
        dir(staticSrcLocation) {            
            if (env.BRANCH_NAME != 'development') {
                sh 'mvn clean deploy'
                archive 'dist/*compressed.zip'
            }else {
                sshagent([gitCredentialsKey]) {
                    sh "rm -rf dist/*"
                    setGitUser()
                    sh "mvn release:prepare"
                    publishNPM()
                    sh "mvn release:perform"
                    if(bm) {
	                    sh "mvn -f ../ -Dincludes=nl.rabobank.gict.mcv.damage_insurance*:* versions:use-releases"
	                    sh "mvn -f ../ -Dincludes=nl.rabobank.gict.mcv.damage_insurance:* versions:use-latest-releases"                   
	                    sh "git commit -am 'Updated FE to latest releases'"
	                    sh "git push origin development"
                    }
                }
            }
        }
    } else {
        echo "No frontend changes so skipping statics build.."
    }
}


private void publishNPM(){
    sh 'gulp release'
    String version = getVersion()
    String versionCommit = "NPM publish v" + version
    sh "git commit -am '${versionCommit}'"
    sh "git tag -m 'NPM publish' -a npm_v${version}"
    sh "git push origin npm_v${version}"
    sh "git push origin development"   
}

/**
 * Function to test java backend with restAssured
 */
def testRestAssured(){
    sh 'rm -rf *'  
    checkout()
    try{
        sh 'mvn -f test/is/rest test'
    }catch(error){
       report()
       throw error
    }
    report()
}

private void report(){
    step([$class: 'JUnitResultArchiver', testResults: 'test/is/rest/target/results.xml'])
}

def startDockerBm(String job, String branch, String bmName, String buildNumber, String version, String port){
    def app = "http://lsrv5484.linux.rabobank.nl/job/"+job+"/branch/"+branch+"/"+buildNumber+"/artifact/artifact/"+bmName+"-is-ear.ear"
    def statics = "http://lsrv5484.linux.rabobank.nl/job/"+job+"/branch/"+branch+"/"+buildNumber+"/artifact/container/is/content/target/"+bmName+"-container-is-content-"+version+"-compressed.zip"
    def properties = "http://lsrv5484.linux.rabobank.nl/job/"+job+"/branch/"+branch+"/"+buildNumber+"/artifact/container/is/properties/artifact/"+bmName+"-container-is-properties.tar.gz"
    sh "docker run -d -p "+port+":9000 --privileged -v /appl/anvci/pif:/opt/pif/dockermap docker-registry.linux.rabobank.nl/aenv/pif-wlp-aiep:66.0 > pif.cid"
    sh 'docker exec `cat pif.cid` bin/sh -c "sudo /opt/pif/dockermap/start.sh ${app} ${statics} ${bmName}-container-is-content ${properties}"'
    readFile('pif.cid')
}

def startDockerCirp(String branch, String buildNumber, String version){
    String app = "http://lsrv5484.linux.rabobank.nl/job/cirp/branch/"+branch+"/"+buildNumber+"/artifact/artifact/pifcirp.ear"
    String statics = "http://lsrv5484.linux.rabobank.nl/job/cirp/branch/"+branch+"/"+buildNumber+"/artifact/presentation/is/content/target/sales-configure-damageinsurance-retail-is-presentation-content-"+version+"-compressed.zip"
    String properties = "http://lsrv5484.linux.rabobank.nl/job/cirp/branch/"+branch+"/"+buildNumber+"/artifact/presentation/is/artifact/sales-configure-damageinsurance-retail-is-presentation-properties.tar.gz"
    String stubProperties = "http://lsrv5484.linux.rabobank.nl/job/cirp/branch/"+branch+"/"+buildNumber+"/artifact/stub/pifcirpst/stub-properties/target/artifact/pifcirp-stub-properties-"+version+".tar.gz"
    String stub = "http://lsrv5484.linux.rabobank.nl/job/cirp/branch/"+branch+"/"+buildNumber+"/artifact/artifact/pifcirpst.ear"
    sh "docker run -d -p 11000-11999:9000 --privileged -v /appl/anvci/pif:/opt/pif/dockermap docker-registry.linux.rabobank.nl/aenv/pif-wlp-aiep:66.0 > pifcirp.cid"
    sh "docker exec `cat pifcirp.cid` sudo bin/sh -c 'sudo /opt/pif/dockermap/start.sh ${app} ${statics} sales-configure-damageinsurance-retail-is-presentation-content ${properties} ${stub} ${stubProperties}'"
    readFile('pifcirp.cid')
}

def getDockerContainerPort(){
    sh "docker port `cat pifcirp.cid` 9000 > port.cid"
    sh 'sed -i "s/^.*://" port.cid' 
    readFile('port.cid')
}

public void loadDockerStubData(String port){
    port = port.replaceAll("[\n\r]", "")
    echo port
    sh "sed -i 's/>tl72</>docker</' pom.xml"
    sh "sed -i 's/DOCKER_PORT/'${port}'/' test/is/testdata/config.json"
    sh "mvn verify -Pload-test-data"
}

private boolean changesInPath(String path) {
    return !lastCommitOnPathContains(path, author) && !lastCommitOnPathContains(path, "updated properties to latest releases") 
}

def onlyReleasePluginCommitsInPaths(def paths) {
	for (path in paths) {
	    if(changesInPath(path)) {
	    echo 'No anvci commit found in path:' + path
	    return false
	    }
	}
    return true
}

private boolean lastCommitOnPathContains(String path, String value) {
    sh "git log -1 -- ${path} > lastCommit.txt"
    def log = readFile("lastCommit.txt")
    boolean result = log.contains(value)
    sh 'rm -rf lastCommit.txt'
    return result
}

public mayIBuild(String jobName) {
	return  !isBuildRunning(jobName);
}

public boolean isBuildRunning(String jobName) {
	boolean result = false;
	def hi = hudson.model.Hudson.instance
	   hi.getItems().each {project ->     
			project.getAllJobs().each {job -> 
			  if (job.isBuilding()) {              
			   if (job.isBuilding() && job.getFullName().equals(jobName)){
					result = true;               
				}
			  }
			}     
		}
	return result;
}

def setGitUser(){
    sh "git config --global user.email '${email}'"
    sh "git config --global user.name '${author}'"
}

def destroyDocker(String containerId){
    sh "docker rm -f ${containerId}"
}

def getVersion(){
    sh "\$(node -e \"console.log(require('./package.json').version)\" > version.txt)"
    readFile("version.txt")
}

def getPomVersion() {
    def matcher = readFile('pom.xml') =~ '<version>(.+)</version>'
    matcher ? matcher[1][1] : null
}

def getPomVersion(String fullPathToPomFile) {
    def matcher = readFile(fullPathToPomFile) =~ '<version>(.+)</version>'
    matcher ? matcher[1][1] : null
}

def getPomArtifact() {
    def matcher = readFile('pom.xml') =~ '<artifactId>(.+)</artifactId>'
    matcher ? matcher[1][1] : null
}

def getPomArtifact(String fullPathToPomFile) {
    def matcher = readFile(fullPathToPomFile) =~ '<artifactId>(.+)</artifactId>'
    matcher ? matcher[1][1] : null
}

def updateComponents(String properties) {
	build job: 'update_components_bm', parameters: [[$class: 'StringParameterValue', name: 'properties', value: properties]]
}
def runE2E(String repo) {
	build job: 'e2e',
	parameters: [
		[$class: 'StringParameterValue', name: 'project', value: repo], [$class: 'StringParameterValue', name: 'type', value: 'smoke']
	]  
}
return this

Resultaten


  • Per commit naar de feature branch kan een build- en featuretest/deploy gedraaid worden afhankelijk van het type branch dit zorgt voor snellere detectie van integratiefouten.
  • Buildstraat wordt gestuurd vanuit code niet meer vanuit losse UI build taken :-)
  • Generieke script library zorgt voor veel 'out of the box' support  voor nieuwe teams
  • Onderhoud generieke library dient goed afgestemd te worden tussen de teams

  

 

 

Workshop

Bouw zelf een pipeline en bouw en test deze op basis van docker containers!

 

 

 

CI met docker, jenkins en pipeline scripts

By Joost van Dieten

CI met docker, jenkins en pipeline scripts

Docker maakt het mogelijk om een schaalbare jenkins omgeving per team in te richten. Hier kunnen dan build slaves worden toegevoegd met verschillende tools naar behoefte van de teams. Door Jenkins pipelines kunnen verschillende branches binnen een repository worden gecontroleerd op code kwaliteit en nog veel meer. Het uitscripten van pipelines zorgt ervoor dat er geen losse buildtaken meer hoeven te worden aangemaakt en de CI straat multibranch coverage kan bieden.

  • 683