Un des points génants lors de l’utilisation de Jenkins est le coté volatile de la configuration des jobs de builds. Il est souvent nécessaire de jouer de click-click pour faire la configuration des jobs sur Jenkins et de se reposer sur un plugin permettant de versionner, autant que possible, les configurations utilisées.

Mais, une fois que vous aurez lu cet article, vous vous rendrez compte que c’est le passé. Attention toutefois, cet article parle de Jenkins, de Docker et de Groovy, n’ayez pas peur, tout est presque trop simple…​

Prérequis Jenkins

Jenkins avec accès à Docker

Nous avons l’habitude d’utiliser un Jenkins lancé dans un container depuis quelques temps.

Nous utilisons l’image maintenue par Michael Bitard agileek/docker-jenkins.

Nous lançons cette image en lui fournissant de quoi exécuter le binaire docker client sans soucis :

docker run
       -d --restart="always" --name jenkins
       -u $(id -u):$(getent group docker | cut -d: -f3) # (1)
       -p 8080:8080
       -v /var/jenkins_home:/var/jenkins_home # (2)
       -v $(which docker):/usr/bin/docker # (3)
       -v /var/run/docker.sock:/var/run/docker.sock # (4)
       -v /usr/lib/x86_64-linux-gnu/libapparmor.so.1:/lib/x86_64-linux-gnu/libapparmor.so.1 # (5)
       agileek/docker-jenkins # (6)
  1. Le container est lancé avec l’utilisateur courant et le groupe docker pour pouvoir accéder au docker.sock

  2. Pour éviter les incohérences de chemin, le chemin racine du jenkins est le même en dehors et dans le container

  3. Le binaire docker du système est fourni dans l’image

  4. Le socket docker est également fourni pour que le client puisse "parler" au démon

  5. La bibliotheque apparmor est nécessaire pour le bon fonctionnement de docker client

Jenkins workflow plugin

Pour utiliser la suite des éléments, vous aurez besoin des plugins gérant la notion de workflow dans Jenkins :

Unresolved directive in #excerpt - include::site/static/lightbox.adoc[]

Ensuite, il nous est possible de créer un job de construction de type workflow :

Unresolved directive in #excerpt - include::site/static/lightbox.adoc[]

Premier job Workflow

Ensuite, c’est là que la magie opère, plutôt que de devoir sélectionner les n-items voulus et remplir chaque étape du build, nous pouvons maintenant le décrire en utilisant du code ! Ainsi, en copiant/collant le script suivant dans la partie idoine, vous devriez avoir un job bien configuré qui marche, du premier coup !

def m2Repo = '-v /var/jenkins_home/.m2:/home/jenkins/.m2' //  # (1)
def timezone = '-e TZ=Europe/Paris' // # (2)
docker.image("codetroopers/jenkins-slave-jdk8-restx")
    .inside("${m2Repo} ${timezone}"){ //  # (3)
    git branch: 'master', url: 'https://github.com/code-troopers/jenkins-workflow-demo-repo.git' // # (4)
    sh "MAVEN_OPTS=-Dfile.encoding=UTF-8 mvn clean install -B -Ppackage" // # (5)
    step([$class: 'ArtifactArchiver', artifacts: 'srv/target/dependency/webapp-runner.jar, srv/target/*.war, run.sh']) // # (6)
    step([$class: 'JUnitResultArchiver', testResults: '**/target/surefire-reports/TEST-*.xml']) // # (7)
}
  1. Partage du dépôt Maven local (pour gagner en temps de build)

  2. Export de la timezone (pour les tests unitaires de l’exemple)

  3. Démarrage du conteneur de build avec la bonne timezone ainsi que le dépôt partagé

  4. Clonage des sources

  5. Lancement du build (en forçant l’UTF-8)

  6. Archivage des produits du build

  7. Archivage des résultats des tests

Comme vous pouvez le voir, le script est relativement parlant et permet en plus de s’affranchir du clickodrome de configuration dans l’interface de Jenkins !

Il est intéressant de noter que l’image Docker qui sert au build est une image personnalisée. Ce n’est pas parce qu’elle inclut un quelconque fonctionnement permettant de builder en utilisant le plugin Workflow. Elle sert de base uniquement car elle met à disposition la partie npm nécessaire au build de la partie frontend de l’application RestX.

Grouper les étapes

Le plugin workflow permet en plus de grouper les différentes étapes d’un build pour permettre, par exemple, de le lancer sur plusieurs environnement différents. Ici nous ajoutons simplement un nom de groupe pour notre étape de build.

stage 'build' // # (1)
    def m2Repo = '-v /var/jenkins_home/.m2:/home/jenkins/.m2'
    def timezone = '-e TZ=Europe/Paris'
    docker.image("codetroopers/jenkins-slave-jdk8-restx").inside("${m2Repo} ${timezone}"){
        git branch: 'master', url: 'https://github.com/code-troopers/jenkins-workflow-demo-repo.git'
        sh "MAVEN_OPTS=-Dfile.encoding=UTF-8 mvn clean install -B -Ppackage"
        step([$class: 'ArtifactArchiver', artifacts: 'srv/target/dependency/webapp-runner.jar, srv/target/*.war, run.sh'])
        step([$class: 'JUnitResultArchiver', testResults: '**/target/surefire-reports/TEST-*.xml'])
    }
  1. Étape nommée pour l’exécution de la construction de l’application

Mettre de côté les fichiers pour plus tard

La notion de stash bien connue des utilisateurs de git est également présente. Elle permet de mettre de côté des fichiers pour les réutiliser à une étape ultérieure du workflow de build. Ceci permet d’éviter l’archivage de produits du build pour des raisons "techniques".

stage 'build'
    def m2Repo = '-v /var/jenkins_home/.m2:/home/jenkins/.m2'
    def timezone = '-e TZ=Europe/Paris'
    docker.image("codetroopers/jenkins-slave-jdk8-restx").inside("${m2Repo} ${timezone}"){
        git branch: 'master', url: 'https://github.com/code-troopers/jenkins-workflow-demo-repo.git'
        sh "MAVEN_OPTS=-Dfile.encoding=UTF-8 mvn clean install -B -Ppackage"
        step([$class: 'ArtifactArchiver', artifacts: 'srv/target/dependency/webapp-runner.jar, srv/target/*.war, run.sh'])
        step([$class: 'JUnitResultArchiver', testResults: '**/target/surefire-reports/TEST-*.xml'])
        stash includes: 'run.sh,srv/target/dependency/webapp-runner.jar,srv/target/*.war,Dockerfile', name: 'dockerBuild' // # (1)
    }
  1. Enregistrement d’une liste de fichiers associée à un nom pour une utilisation ultérieure

Étape de construction d’une image Docker

stage 'docker' // # (1)
node{ // # (2)
  ws{ // # (3)
    unstash 'dockerBuild' // # (4)
    docker.build("codetroopers/jenkins-workflow-demo:${env.BUILD_ID}") // # (5)
  }
}
  1. Création d’une nouvelle étape

  2. Permet de distinguer un ensemble d’opération de build (peut accepter les labels pour restreindre sur des noeuds)

  3. Déclenche la création d’un nouveau workspace

  4. Récupère les fichiers mis de côté sous le nom dockerBuild

  5. Construction d’une image docker avec pour tag le numéro de build en cours ($BUILD_ID)

Workflow et gestion multibranche

Dans nos façons de fonctionner qui sont maintenant devenues habituelles, nous utilisons de façon intensives les branches pour isoler nos développements. Un des points fastidieux est de configurer un nouveau job Jenkins pour chaque branche afin de valider son bon fonctionnement et ne pas se rendre compte trop tard d’un build au rouge.

Le plugin 'Workflow Multibranch' simplifie de façon drastique ce genre de cas, il suffit de rajouter un descripteur de build dans les sources. Le fichier correspondant est tout simplement appelé Jenkinsfile.

stage 'build'
    def m2Repo = '-v /var/jenkins_home/.m2:/home/jenkins/.m2'
    def timezone = '-e TZ=Europe/Paris'
    docker.image("codetroopers/jenkins-slave-jdk8-restx").inside("${m2Repo} ${timezone}"){
        checkout scm // # (1)
        sh "MAVEN_OPTS=-Dfile.encoding=UTF-8 mvn clean install -B -Ppackage"
        step([$class: 'ArtifactArchiver', artifacts: 'srv/target/dependency/webapp-runner.jar, srv/target/*.war, run.sh'])
        step([$class: 'JUnitResultArchiver', testResults: '**/target/surefire-reports/TEST-*.xml'])
        stash includes: 'run.sh,srv/target/dependency/webapp-runner.jar,srv/target/*.war,Dockerfile', name: 'dockerBuild'
    }

stage 'docker'
node{
  ws{
    unstash 'dockerBuild'
    docker.build("codetroopers/jenkins-workflow-demo:${env.BUILD_ID}")
  }
}
  1. Il faut bien entendu remplacer l’endroit où nous faisions le git clone pour qu’il soit dynamique par rapport à ce qu’on construit. Le terme checkout scm permet de s’assurer de ce fonctionnement.

L’intérêt est que chaque branche qui sera buildée n’aura pas son historique mélangé avec une autre (là où les jobs de validation de Pull Request ont tendance à tout mélanger). De plus, un changement dans le process de build sera directement versionné. Il n’y aura donc pas besoin de penser à éditer le job lors du merge sur master (on a tous vécu ce genre de situation énervante) !

Attendre une confirmation utilisateur

Un des points intéressant de ce plugin est qu’il permet la mise en pause des constructions. Ainsi, il est possible de mettre en pause une construction correspondant à une livraison et de lui faire attendre une validation manuelle par exemple.

stage 'build'
    def m2Repo = '-v /var/jenkins_home/.m2:/home/jenkins/.m2'
    def timezone = '-e TZ=Europe/Paris'
    docker.image("codetroopers/jenkins-slave-jdk8-restx").inside("${m2Repo} ${timezone}"){
        git branch: 'master', url: 'https://github.com/code-troopers/jenkins-workflow-demo-repo.git'
        sh "MAVEN_OPTS=-Dfile.encoding=UTF-8 mvn clean install -B -Ppackage"
        step([$class: 'ArtifactArchiver', artifacts: 'srv/target/dependency/webapp-runner.jar, srv/target/*.war, run.sh'])
        step([$class: 'JUnitResultArchiver', testResults: '**/target/surefire-reports/TEST-*.xml'])
        stash includes: 'run.sh,srv/target/dependency/webapp-runner.jar,srv/target/*.war,Dockerfile', name: 'dockerBuild'
    }

stage 'docker'
node{
    ws{
        unstash 'dockerBuild'
        def built = docker.build("codetroopers/jenkins-workflow-demo:${env.BUILD_ID}")
        input 'Is everything ok ? Run app ?' // # (1)
        echo "We can run the docker-compose up here"
        def outcome = input message: 'We can even have parameters to answer this question', parameters: [ // # (2)
            [name: 'myChoice', description: 'My choice', choices: 'Choice 1\nChoice 2\nChoice 3', $class: 'ChoiceParameterDefinition']
        ]
        echo "You have chosen ${outcome}" // # (3)
    }
}
  1. input met en pause la construction et permet de continuer ou interrompre celle-ci

  2. Il est également possible de permettre à l’utilisateur de faire un choix

  3. Ici la valeur sélectionnée par l’utilisateur est écrite dans la sortie du build.

J’espère que cet article vous donnera l’envie d’essayer de rationnaliser un peu plus la configuration de vos job Jenkins en les stockant dans votre SCM

Proxying with Docker

When using docker under a corporate proxy, it can be cumbersome to have a working networking in all containers. You often end up being blocked by specific network access which does not seem to be properly forwarded to the proper proxy. For example when using apt.

Classic way of doing

There is a documented way of using a proxy, by adding command-line switches to your docker deamon. However, it does not seem to work everytime and could require exporting additional settings to your in-container applications (in my experience though).

Why not using docker

Nicolas pointed me an image he created to help with the setup of a corporate proxy. It uses redsocks under the hood that listen to the docker socket and automatically add the glue to do the forwarding through the proxy.

Easy proxying in docker is just one command away ! (fill in the blank of your proxy ip and port)

docker run \
       --restart=always \
       --privileged=true \
       --net=host \
       -d ncarlier/redsocks \
       $PROXY_IP $PROXY_PORT

Multi Hop

It is often required that, for security reason, you have to hop through a SSH gateway to access other machines. While this is perfectly fine and simple to do, it is often cumbersome to open a new session. However, with a small script you can speed up your access to machines even with such a restriction in place.

Classical way of hop’ing

Let’s say our gateway is named gateway and our target host myAppHost the classical way of doing it would be :

ssh gateway
you@gateway $ hostname
gateway.my.tld
you@gateway $ ssh myAppHost
you@myAppHost $ hostname
myAppHost.my.tld
   

Faster way of hop’ing

A quicker way of doing this is to specify the ssh command directly, there is one thing to tell ssh though: allocating a TTY even if it does not seem to be connected to one. In fact, the command supplied to ssh is not supposed to be interactive, that is why you need to give this hint to SSH :

ssh -t gateway ssh myAppHost
you@myAppHost $ hostname
myAppHost.my.tld
   

Script this !

The script is really simple, and only consists in the following

#!/bin/sh
ssh -t gateway ssh $1
   

Save this in your path and give it the run permission then you are all set (mine is named gssh). All you have to do to connect is now a simple gssh myAppHost

Maven testing

One of the bothering thing being a contractor is that you often happen to work on a project with a skip tests flag set on all developers computer.

One of the thing I tend to do when on such project is enabling tests and trying to fix as much as possible (often the fixes are easy to do).

Multi module testing

By design, surefire plugin make the build fail if there is a test failure. While this is ok in single module, when working with multi-module project it can be nice to run all tests on all modules regardless of the failures happening in some modules.

Maven is a great tool and allows such a behavior very easily, it allows two command line switches for that :

  • --fail-at-end : will fail the build at the end if there is test failures
  • --fail-never : will never fail the build, even if there is test failures

Flags behavior differences

There is one thing to understand when using the --fail-at-end flag, it will fail the build at end for a module with test failure but it will also prevents building of dependent modules.

With a small example it become obvious. Let’s say that we have a multi project containing the following :

  • core : containing model objects and services
  • web : containing web views for browser access
  • javafx : containing desktop application classes

It is straightforward to see that web and javafx modules will depends on the core module.

fail-at-end

If using the --fail-at-end flag, a test failure in the core module will prevent building the web and javafx module completely : you will not be able to track tests failure before fixing the ones from core (at least on a single build command).

fail-never

If using the --fail-never flag, a test failure in the core module will be reported but the build and tests of the web and javafx modules will be built and their respective tests errors will also be reported.

Tired of typing

If you find that typing --fail-at-end is too long, remember yourself it short alias : -fae.

The same is also available for the --fail-never flag with : -fn.

Switching keymap

As some of you might know, I am now using a Typematrix 2030 on a daily basis. When I switched to this great keyboard I also adopted a new layout on it : Colemak.

However, to be able to pair with others not using a Colemak mapping, I did not set the default mapping to Colemak but I instead use an udev rule to set the input method to Colemak only for the Typematrix.

Write a keymap switch script

Thanks to @BitardMichael tips and existing scripts I ended up with the following script saved in /usr/local/bin/set_typematrix_colemak_mapping

One of the tricky part was having a way of executing the script only when the keyboard is ready, without blocking udev’s job (or the keyboard is not yet visible by the X system). The workaround I found was scheduling the execution of the job with the simple at command.

Tell udev to run the script on keyboard detection

The process is really easy, all you need to do is to add the following to a new file : /lib/udev/rules.d/85-typematrix.rules

Adapting it to your use case

If you are using another keyboard than a Typematrix you will need to adapt the udev rule with the proper Vendor / Product IDs (you can grab them with lsusb). For the xinput part, you will need to adjust the grep to match your hardware.

Of course the same goes for your layout : colemak / dvorak / bépo …