Analyse des Jenkinsfile par SonarQube !

Jenkinsfile pourquoi faire ?

Le jenkinsfile est le fichier présent dans le SCM de votre projet qui permet de décrire la pipeline attendu dans votre jenkins : terminé les jobs traditionnels configurés sur IHM, bonjour les pipeline jobs ! Écrits en Groovy, vous décrivez l'enchaînement et le parallélisme des instructions. Toutes les étapes CI/CD peuvent être présentes : un git pull, un mvn package, une analyse qualité du projet ...

Il existe deux façons d’écrire un Jenkinsfile : Declarative ou Scripted. Comme expliqué sur la page https://jenkins.io/doc/book/pipeline/syntax/#compare, le mode Declarative est préférable pour le débutant car il offre une série de mots-clés prédéfinis et est idéal pour des pipelines simples. Le mode Scripted est bien plus puissant et possède beaucoup moins de limites : vous codez directement le pipeline en Groovy.

La liberté offerte par le mode Scripted crée un risque de glisser quelques maladresses pouvant nuire au projet et même mettre à mal votre instance Jenkins : éléments de configuration mal initialisés, des instructions au mauvais endroit qui bloquent un nœud inutilement … Cloudbees récapitule quelques bonnes règles :
https://www.cloudbees.com/blog/top-10-best-practices-jenkins-pipeline-plugin

Il existe déjà des linter pour Jenkinsfile ( https://jenkins.io/doc/book/pipeline/development/ ), mais ils relèvent plus de l’analyse de syntaxe que du détecteur de mauvaises pratiques.
En prenant comme exemple le point 7 dans le top 10 de Cloudbees (ne pas utiliser d’instruction ‘input’ au sein d’un ‘node()’) nous pouvons écrire la règle correspondante pour Sonarqube.

SonarQuoi ?

Sonarqube ( https://www.sonarqube.org/ ) est un outil permettant de mesurer la qualité de votre projet : évaluer sa dette technique, mettre en lumière les mauvaises pratiques et les bugs potentiels. Avec son interface vous avez une synthèse précise quant à la qualité de votre projet. Vous pouvez parcourir les anomalies, les trier par sévérité (bloquante, critique, … ), et même fixer un seuil de qualité ( le quality gate ) à partir duquel votre build ( maven ou autre ) sera terminé en erreur.
Supportant 15 langages dans sa version open-source il est possible d’étendre les jeux de règles par son système de plugins. Les jenkinsfile étant écrits en Groovy, nous allons ainsi enrichir d’une nouvelle règle le plugin dédié à ce même langage ...

CodeNarc

Pour analyser du Groovy, le plugin Sonarqube se base sur le projet CodeNarc https://github.com/CodeNarc/CodeNarc bien connu par la communauté des développeurs. Il peut fonctionner de manière ‘stand-alone’, en ligne de commande et il contient un nombre conséquents de règles qualités. Dans le cadre du plugin sonarqube-groovy il agit en tant qu’élement moteur : Sonarqube invoque CodeNarc puis met en forme les résultats d’analyse pour les mettre à disposition dans son IHM.

Le plugin Sonarqube travaille avec CodeNarc v0.25.2, nous nous baserons donc sur cette version pour enrichir CodeNarc avec notre nouvelle règle : https://github.com/CodeNarc/CodeNarc/releases/tag/v0.25.2

Comment il marche ?

Pour chaque classe, CodeNarc transforme le code source en un arbre ou arborescence, appelée Abstract Syntax Tree (AST). Chaque feuille représentant un élément du code : import, declaration, boucle…

Dans une règle CodeNarc il est possible de réagir lors de la présence d’une feuille précise, laissant la main pour contrôler ce qui la compose. Par exemple on s'abonne à un noeud de type import et on peut vérifier la valeur de l’import. Les ‘abonnements’ suivants sont disponibles :
http://docs.groovy-lang.org/docs/groovy-2.1.8/html/api/org/codehaus/groovy/ast/CodeVisitorSupport.html

Mise en place du futur rulesset ( optionnel )

Un rulesset est en fait un dossier de règles : pour la bonne organisation du projet, il est possible de créer une règle dans un ruleset déjà existant comme ‘basic’ ou bien dans un nouveau que l’on appellerait au hasard ‘Jenkinsfile’. :)

mkdir CodeNarc-0.25.2\src\main\groovy\org\codenarc\rule\jenkinsfile
mkdir CodeNarc-0.25.2\src\test\groovy\org\codenarc\rule\jenkinsfile

Il faut également initier le ruleset dans le répertoire CodeNarc-0.25.2\src\main\resources\rulesets pour cela on y crée un nouveau fichier jenkinsfile.xml avec le contenu minimaliste suivant :

<ruleset xmlns="http://codenarc.org/ruleset/1.0"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://codenarc.org/ruleset/1.0  http://codenarc.org/ruleset-schema.xsd"
    	xsi:noNamespaceSchemaLocation="http://codenarc.org/ruleset-schema.xsd">
	<description>Jenkinsfile rule set.</description>
</ruleset>

Création d’une règle à partir du templating

Les auteurs de CodeNarc ont ajouté une classe utilitaire pour simplifier la création d’une règle : (à exécuter à la racine du projet)

groovy codenarc.groovy create-rule

Exemple de réponses aux questions :
Enter your name:Jon Snow

Enter the rule name:CheckUserInputInNode

Enter the rule category. Valid categories are:
  basic
  braces
  comments
  concurrency
  convention
  design
  dry
  enhanced
  exceptions
  formatting
  generic
  grails
  groovyism
  imports
  jdbc
  jenkinsfile
  junit
  logging
  naming
  security
  serialization
  size
  unnecessary
  unused
( choisissez le rulesset de votre choix jenkinsfile, basic, … )

Enter the rule description: ( apparaîtra dans les commentaires d’en tête de la future classe ) “Check that there is no ‘input’ statement in a ‘node’ block.”

Si une erreur Git survient, ce n’est pas grave, il s’agit juste de l’action create-rule qui essaye de déclarer dans le projet Git les nouveaux ajouts.

Voilà, deux classes, implémentation et test viennent d’être générées :

CheckUserInputInNodeRule.groovy dans CodeNarc-0.25.2\src\main\groovy\org\codenarc\rule[rulesset]

CheckUserInputInNodeRuleTest.groovy dans CodeNarc-0.25.2\src\test\groovy\org\codenarc\rule[rulesset]

Édition du ‘build.gradle’

Nous ne réaliserons pas de release dans cet article, de ce fait nous commenterons :

  • la ligne id 'net.researchgate.release' version '2.0.2',
  • la partie release à la fin du fichier.

Implémentation de la règle

Notre règle doit répondre à la question suivante : existe-t-il un élément ‘node’ qui contient une instruction ‘input’ ?

Pour le savoir, il faut s’abonner au type de nœud qui voit passer la déclaration de ‘node’ et de ’input’ : comme le montre le catalogue http://docs.groovy-lang.org/docs/groovy-2.1.8/html/api/org/codehaus/groovy/ast/CodeVisitorSupport.html il existe une méthode visitMethodCallExpression fort intérressante que nous allons surcharger : en effet elle réagit à une instruction appel de méthode du genre foo(param1,..), ce qui match avec input(id: 'userInput', … ) ainsi qu’à node('DOCKER'){ ..} .

import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression

class CheckUserInputInNodeAstVisitor extends AbstractAstVisitor {

int startBlockNode
int endBlockNode
	 
@Override
void visitMethodCallExpression(MethodCallExpression mce){

	ConstantExpression ce =  (ConstantExpression)mce.getMethod()
	
	if ("node".equals(ce.getText())){
          // un élément ‘node’ !
          // le node s'étend de quelle ligne à quelle ligne ?
          startBlockNode=mce.lineNumber
          endBlockNode=mce.lastLineNumber
	}

	if ( "input".equals(ce.getText())){
          // un élément ‘input’ !
          // l'élément ‘input’ est il compris dans l'élément ‘node’ ?
		  if (ce.lineNumber >= startBlockNode && ce.lineNumber<=endBlockNode){
			// création d’une violation
			addViolation(ce, 'User input detected in node !')
		  }
	}
	super.visitMethodCallExpression(mce)
}

Test

Pour la classe de test, nous complétons les deux méthodes de tests pré-initialisées, avec du code source pour tester l’implémentation de la règle.

Pour la partie // todo: replace with source for passing edge case(s) nous plaçons un code source qui passe avec succès le test :

stage('test'){
    userInput = input(id: 'userInput',
    message: 'Do you want to release this build?',
    parameters: [[$class: 'PasswordParameterDefinition',
        defaultValue: "",
        name: 'password',
        description: 'Password']])

    sh "echo User Input is" + userInput	
}

Pour la partie // todo: replace with source that triggers a violation avec un code source qui lèvera une violation :

node('DOCKER'){
    stage('test'){
        userInput = input(id: 'userInput',
        message: 'Do you want to release this build?',
        parameters: [[$class: 'PasswordParameterDefinition',
            defaultValue: "",
            name: 'password',
            description: 'Password']])

        sh "echo User Input is" + userInput
    }
}

La condition assertSingleViolation(SOURCE, 1, '...') juste en dessous peut être remplacée par assertSingleViolation(SOURCE, 4 ). En effet, en accord avec la logique du test nous nous attendons à une erreur unique située à la ligne 4 du code source.

La partie testMultipleViolations peut être supprimée, les deux tests précédents étant suffisants.

Exécution de la classe de test :

gradlew test --tests org.codenarc.rule.jenkinsfile.CheckUserInputInNodeRuleTest

ajoutez --info pour avoir une sortie plus verbeuse.

Il est ainsi facile d’œuvrer en TDD jusqu'à obtenir des tests en succès : org.codenarc.rule.jenkinsfile.CheckUserInputInNodeRuleTest > testSingleViolation PASSED

Construction et mise au chaud dans le repository d’artefacts

Dans le fichier build.gradle, remplaçons la version que nous construisons par "0.25.2-custom".

Construction de notre archive CodeNarc custom:

gradlew install -x test -x signArchives

Le goal 'install' va, comme avec Maven, placer notre nouvelle archive dans le repo local .m2

Plugin Sonarqube pour le langage Groovy

Nous déploierons notre plugin maison sur un sonarqube 6.7.6 LTS disponible au moment de la rédaction de cet article. ( https://www.sonarqube.org/downloads/ )

Dans le plugin ‘sonar-groovy’ https://github.com/pmayweg/sonar-groovy (branche master) il nous reste à indiquer l'existence de notre nouvelle règle pour faire le lien avec le moteur CodeNarc, ainsi qu’à faire prendre en compte les fichiers sans extension, cas particulier des ‘Jenkinsfile’.

Important:
Si vous préférez utiliser la version 7 de Sonarqube utilisez le fork de Adam Bertrand, qui prend en charge les modifications des API de plugins Sonarqube pas encore passées en LTS : https://github.com/Hydragyrum/sonar-groovy/tree/replace-rules-profile-7.5

CodeNarc intégré dans le plugin Sonarqube

Le plugin Sonarqube intègre les règles de CodeNarc à l’aide de 3 fichiers présents dans le répertoire sonar-groovy-master\sonar-groovy-plugin\src\main\resources\org\sonar\plugins\groovy :

rules.xml

Dans ce fichier xml la règle est déclarée dans le plugin et sera opérationnelle sur l’instance Sonarqube.

<rule>
    <key>org.codenarc.rule.jenkinsfile.CheckUserInputInNodeRule</key>
    <severity>BLOCKER</severity>
    <name><![CDATA[User input in node]]></name>
    <internalKey><![CDATA[CheckUserInputInNode]]></internalKey>
    <description><![CDATA[<p>Checks for user input in Jenkinsfile. </p>
<p>The input element pauses pipeline execution to wait for an approval - either automated or manual. Naturally these approvals could take some time. The node element, on the other hand, acquires and holds a lock on a workspace and heavy weight Jenkins executor - an expensive resource to hold onto while pausing for input.</p>
<p>Here is an example of code that produces a violation: </p>
<pre>
node('DOCKER'){
		stage('test'){
			userInput = input(id: 'userInput',			// violation
			message: 'Do you want to release this build?',
</pre>
<p>This example is nice: </p>
<pre>

		stage('test'){
			userInput = input(id: 'userInput',			
			message: 'Do you want to release this build?'
			node('DOCKER'){
			...
			}
		}

</pre>
]]></description>
    <tag>bug</tag>
  </rule>

Pour la balise ‘key’ indiquez bien le nom de la classe de la règle.
‘severity’ est le niveau de sévérité par défaut. Il peut être redéfini dans un “profil qualité” ( https://docs.sonarqube.org/latest/instance-administration/quality-profiles/ ).
La ‘description’ de la règle est écrite en HTML, pour l’exemple je me suis permis de reprendre la description du point 7 sur le site de Cloudbees ( https://www.cloudbees.com/blog/top-10-best-practices-jenkins-pipeline-plugin
)

cost.csv

Ce fichier .csv contient le poids de chaque règle en terme de remédiation (le coût de l’effort) pour corriger la mauvaise pratique :

org.codenarc.rule.jenkinsfile.CheckUserInputInNodeRule;linear;10min

profile-sonar-way.xml

Par défaut c’est ce profil du même nom qui est utilisé dans sonarqube pour analyser les fichiers groovy. Grâce à ce fichier la règle y est associée, et se voit attribuer un niveau de gravité éventuellement différent de celui défini dans rules.xml.

 <rule>
      <repositoryKey>grvy</repositoryKey>
      <key>org.codenarc.rule.jenkinsfile.CheckUserInputInNodeRule</key>
      <priority>BLOCKER</priority>
 </rule>

Extension du fichier

Cette partie est optionnelle si vous travaillez uniquement avec des fichiers pipeline qui conservent leurs extensions en ‘*.groovy’, par exemple ‘ci.groovy’.

Sonarqube a l’habitude de travailler avec les extensions des fichiers pour y détecter le langage associé et ignore les fichiers sans extensions. Selons les organisations, il peut arriver que les Jenkinsfile soient des fichiers sans extension. Voici comment les faire analyser :

Dans la classe org.sonar.plugins.groovy.foundation.GroovyFileSystem on modifie la déclaration du prédicat isGroovyLanguage, en créant un nouveau prédicat isJenkinsFile:

    this.isJenkinsfile = predicates.and(predicates.matchesPathPattern("**/*enkinsfile*"), predicates.doesNotMatchPathPattern("**.**"));

    this.isGroovyLanguage = predicates.or(predicates.hasLanguage(Groovy.KEY), isJenkinsfile);

Le scanner sonarqube va associer à l’analyse des fichiers .groovy tout ce qui comporte ‘enkinsfile’ sans extension. Dès lors un fichier ‘Jenkinsfile’ ou ‘JenkinsfileV2’ sera bien analysé. Par contre un Jenkinsfile.java ne sera pas scanné en Groovy mais restera bien au scanner du langage Java.

Dans la classe org.sonar.plugins.groovy.codenarc.CodeNarcSensor méthode public void describe, commentez la ligne

    // .onlyOnLanguage(Groovy.KEY)

Même chose dans la classe org.sonar.plugins.groovy.GroovySensor méthode public void describe, commentez la ligne

    // .onlyOnLanguage(Groovy.KEY)

En effet ces lignes restreignent les analyses uniquement au langage Groovy, c’est-à-dire aux fichiers avec extensions “.groovy”.

Construction du plugin

Dans le fichier sonar-groovy-master\sonar-groovy-plugin\pom.xml indiquons la bonne version pour exploiter notre librairie CodeNarc custom fabriquée auparavant :

<dependency>
  	<groupId>org.codeNarc</groupId>
  	<artifactId>CodeNarc</artifactId>
  	<version>0.25.2-custom</version>
</dependency>

Il ne reste plus qu’à construire le plugin pour Sonarqube (instruction à exécuter à la racine du projet) :

mvn clean package -DskipTests

Oui je sais les tests sont ‘skip’, car en l’état le build échouerait. Pour être propre il faut faire évoluer 3 méthodes de tests.
==>Le code avec les tests à jour est disponible sur le repo GitHub de cet article.

Le plugin est prêt à être placé dans le répertoire \extensions\plugins, après un arrêt/relance, la règle doit être visible :

sonarQ01

Analyse d’un projet avec Maven

En principe le Jenkinsfile se situe à la racine du projet.
Dans le cas où vous êtes sûr du nommage du fichier Jenkinsfile vous pouvez le citer expressément :

mvn -U clean package sonar:sonar -Dsonar.sources=Jenkinsfile,pom.xml,src/main

sinon dans le cas où votre ligne de commande est commune à différents projets et que le nommage du Jenkinsfile risque de varier, vous pouvez jouer avec le paramètre -Dsonar.inclusions=*enkins*
afin de couvrir toutes sortes de nommage des fichiers "Jenkinsfile", "JenkinsfileV2", etc

mvn -U clean package sonar:sonar -Dsonar.sources=. -Dsonar.inclusions=*enkins*,src/main/** -Dsonar.exclusions=/target/**

sonarQ02


sonarQ03


sonarQ04

Améliorations

On pourrait imager :

  • une phase de build 100% gradle tout en un,
  • un import automatique des règles CodeNarc dans les fichiers de configuration du plugin Sonarqube car dans le cas présent nous avons déclaré la règle une fois côté CodeNarc puis une nouvelle fois côté plugin Sonarqube,
  • profiter de la dernière version de CodeNarc v1.3 plus moderne.

Conclusion

En prenant l’exemple d’une mauvaise pratique avérée nous avons vu comment concrètement la détecter dans Sonarqube : à votre tour d’enrichir le jeu de règles pour permettre à vos pipelines de gagner en qualité.

De manière générale, pour écrire une nouvelle règle, et quel que soit le langage, vous aurez souvent à manipuler les arbres syntaxiques. C’est dans l’identification des types de nœuds que réside la principale difficulté. Sonarqube propose une documentation pour bien démarrer ( https://docs.sonarqube.org/display/DEV/Adding+Coding+Rules+using+Java ).

Particularité de cet article où nous avons écrit du Groovy pour enrichir le moteur CodeNarc, les autres plugins Sonarqube embarquent leurs propres parseurs d’AST et sont écrits en Java, rendant la tâche plus simple et moins exotique.

L’autre particularité de cet article est que nous nous sommes permis de modifier le plugin pour étendre l’analyse à des fichiers sans extension, ce qui reste peu commun, mais intéressant à connaître.

Au final essayez d’associer Sonarqube à toutes les facettes de votre projet : Java, technos front, Jenkinsfile, ... et exploitez la fonctionnalité du ‘water leak’ pour éviter l’effet ‘mur des bugs’ !

Le code source est disponible sur: https://github.com/maxospiquante/blog_ippon_sonarqube_jenkinsfile