Il est courant maintenant d’utiliser Firebase dans les applications mobiles. Que ce soit pour la partie statistiques, les notifications ou le reporting des crashes. L’intégration est très facile, mais peut devenir compliquée dès lors qu’on manipule des target différentes avec des configurations différentes. Cet article va vous détailler une façon simple mais efficace de gérer ces différences.

Multiples fichiers plist

Les fichiers plist sont des fichiers XML de configuration pour XCode. Lors de la création d’un projet dans Firebase, il est possible de récupérer une version générée contenant les bonnes variables pour votre projet. Le problème qui peut se produire est lorsqu’on génère plusieurs livrables avec des configurations qui doivent être différentes grace aux target. Il n’est pas possible, ni confortable, de gérer le cas de plusieurs fichiers plist dans les sources. La redéfinition du nom du fichier à faire utiliser par Firebase ne marche pas à tous les coups et il a tendance à aller lire la valeur par défaut GoogleService-Info.plist.

L’idée est donc d’utiliser un script au build qui se chargera de configurer correctement le fichier plist qui sera inclu dans le livrable.

Première étape : variabilisation

Pour être capable de générer le fichier plist correctement, il est nécessaire de le variabiliser. Dans mon cas, j’ai identifié les valeurs suivantes qui devaient être variabilisées:

  • GOOGLE_APP_ID

  • BUNDLE_ID

  • CLIENT_ID

  • REVERSED_CLIENT_ID

J’ai donc ajouté des variables à la configuration de mon build pour représenter ces valeurs qui doivent être personnalisées. J’utilise des fichiers .xcconfig mais ceci fonctionne également avec l’ajout manuel (dans Build Settings > + > Add User-Defined settings)

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

En plus de ça, j’ai modifié le fichier GoogleService-Info.plist téléchargé sur Firebase pour enlever les éléments qui allaient être remplacés à terme. Plus exactement, j’ai remplacé les valeurs par des chaînes du type WILL BE REPLACED AT BUILD TIME, qui me permettent facilement de me rendre compte d’un oubli de configuration.

Deuxième étape : génération du fichier

Une fois le fichier et l’environnement préparé, il ne reste qu’à générer la version finale. Pour se faire, je me base sur le système de build de XCode qui permet de définir simplement des étapes. Les variables définies dans la configuration du projet sont en effet disponibles simplement lors de l’exécution des étapes de build. En utilisant l’outil defaults inclu dans macOS pour la manipulation des fichiers plist, il est donc facile de définir les valeurs attendues dans le fichier. J’ai ainsi rajouté une étape shell script correspondant à ceci dans le processus de build.

defaults write "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" GOOGLE_APP_ID ${GOOGLE_APP_ID}
defaults write "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" BUNDLE_ID ${PKG_IDENTIFIER}
defaults write "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" CLIENT_ID ${CLIENT_ID}
defaults write "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" REVERSED_CLIENT_ID ${REVERSED_CLIENT_ID}

Enfin, il faut ordonner cette étape de build après celle de copie des ressources, pour effectuer la modification uniquement dans le fichier binaire de l’application et non dans les sources du projet.

Bénéfices

Il est donc facile de disposer de configuration différente de Firebase entre le debug et la release par exemple. Ainsi vous pouvez tester l’envoi de notification, par exemple, sans risquer de polluer des utilisateurs de production, ou activer le crash reporting uniquement en production par exemple.

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.