Accueil > Code > Écriture d’un plugin Jenkins

Écriture d’un plugin Jenkins

jeudi 18 avril 2013, par Nicolas

Introduction

Jenkins est un très bon outil, dont je me sers pour différentes opérations (capture de pages de site, appel et stockage de statistiques, etc.).

Il a de nombreux plugins, qui font de nombreuses choses.

Pour expérimenter, j’ai voulu faire un petit plugin qui ramassait des statistiques après chaque construction, et affichait des comparaisons. C’est, dans les termes Jenkins, une opération de « post-construction » (« post-build »).

Je me suis basé, outre de nombreuses recherches et expérimentations, sur les billets suivants :
- https://wiki.jenkins-ci.org/display/JENKINS/Plugin+tutorial
- http://blog.codecentric.de/en/2012/08/tutorial-create-a-jenkins-plugin-to-integrate-jenkins-and-nexus-repository/
- http://blog.codecentric.de/en/2013/02/tutorial-jenkins-plugin-development/
- les sources du plugin SLOCCount, qui ressemble à ce que je voulais faire

Les limites de l’exercice sont les suivantes :
- pas de gestion de l’internationalisation
- pas nécessairement la ou les bonnes façons de faire
- le code n’est pas complet, et ne compilera ou fonctionnera pas à toutes les étapes, mais au final il devrait y avoir quelque chose d’utilisable
- aucune information n’est affichée au niveau d’un projet utilisant ce plugin, en revanche des informations sont affichées pour chaque construction du projet

Environnement, pré-requis

J’ai utilisé NetBeans version 7.2, associé avec Maven, l’IDE ouvrant le .pom de Maven sans souci.

La création du plugin a été faite tel que décrit sur la page du wiki Jenkins.

Le groupId et le artifactId sont arbitraires pour un plugin interne.

Les pages HTML affichées à l’utilisateur sont générées via Jelly, syntaxe qui ressemble à JSP.

Le squelette généré permet de lancer Jenkins en mode test, avec le plugin déjà pré-installé, ainsi que de débugger.

Apparemment certaines modifications, depuis NetBeans, sont prises en compte (typiquement celles liées à des .jelly), d’autres demandent un redémarrage du serveur de test.

Première étape : un peu de ménage, bases

Le script Maven va créer un plugin avec une action de build, donc le mieux est de supprimer les fichiers de base HelloWorldBuilder.java et le répertoire src/main/resources/(nom du plugin)/HelloWorldBuilder.

Le seul fichier qui reste sera donc src/main/resources/index.jelly, qui contient la description du plugin telle qu’elle sera affichée dans Jenkins, encapsulé dans un div.

Seconde étape : enregistrer des choses après une construction

L’enregistrement se fait simplement en déclarant une classe qui étend Recorder. Appelons-la TestRecorder.

La méthode à implémenter de façon impérative est basique :

  public BuildStepMonitor getRequiredMonitorService() {
    return BuildStepMonitor.BUILD;
  }

Évidemment le plugin ne fait encore rien. La collecte de données se fait en surchargeant la méthode public boolean perform(AbstractBuild <?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException.

Choses intéressantes à savoir à son propos :
- il n’est a priori pas nécessaire d’appeler l’implémentation de base
- retourner false marque la construction en échec
- pour vérifier l’état du build (échec ou non) au moment de l’étape de post-construction, utiliser build.getResult()
- pour écrire des choses dans la sortie (« console ») retournée à l’utilisateur, utiliser listener.getLogger().println() ou toute autre méthode du logger

Afin de parcourir les fichiers résultant des étapes précédentes, le mieux est apparemment d’utiliser build.getWorkspace().act(), méthode qui prend en paramètre une implémentation de FilePath.FileCallable et retourne une classe spécifiée par l’utilisateur.

C’est dans la méthode invoke de FilePath.FileCallable que l’on récupère un File standard Java pointant sur la racine, avec lequel on peut parcourir les fichiers.

L’objet retourné doit étendre Serializable, car il va être stocké avec la construction. Appelons-le TestResult.

Il faut également qu’il ait un lien vers le build auquel il est rattaché, avec une méthode public AbstractBuild <?,?> getOwner() retournant cet objet (voir plus loin l’explication).

Troisième étape : déclarer le plugin

La déclaration se fait en déclarant une classe qui étend BuildStepDescriptor, avec les particularités suivantes :
- la classe doit être annotée avec @Extension
- le constructeur doit appeler super(TestRecorder.class) ;
- isApplicable(Class< ? extends AbstractProject> type) permet de limiter les types de construction auxquels le plugin s’applique. Ici on peut simplement retourner true
- enfin getDisplayName() retourne le nom à afficher dans la liste d’opérations de post-construction.

Quatrième étape : afficher un lien sur la colonne de gauche d’une construction

Il faut créer une classe implémentant les interfaces Action, Serializable et StaplerProxy. Appelons-la TestAction.

Les méthodes à implémenter sont :
- getDisplayName(), qui fournit le nom qui sera affiché dans la colonne de gauche d’une construction, par exemple « plugin de test »
- getUrlName(), qui donne le répertoire virtuel qui apparaîtra dans l’URL, par exemple « plugin-test »
- getIconFileName(), qui fournit l’URL d’une icône, au format 24x24 pixels (il est possible de retourner une chaîne vide, aucune icône ne sera affichée)
- getTarget(), qui retourne un objet qui sera affiché sur la page, nous allons ici utiliser notre TestResult qu’il faut donc passer en paramètre de constructeur de notre TestAction.

À ce stade, le plugin doit pouvoir se construire, l’étape être ajoutée à une post-construction dans la configuration d’un job.

Afin d’afficher un lien dans la colonne de gauche, il faut durant le perform appeler build.addAction(new TestAction(testResult)).

Cet objet TestAction, sérialisé dans le build, provoquera l’affichage du lien, qui n’est pas encore fonctionnel.

Cinquième étape : implémenter la page d’informations sur la construction

Pour rendre le lien fonctionnel, il faut créer un fichier src/main/resources/(package)/TestResult/index.jelly. Dans ce fichier on pourra mettre ce que l’on veut, en HTML et ou Jelly, pour afficher la page, mais typiquement :

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"
        xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson"
        xmlns:f="/lib/form" xmlns:i="jelly:fmt">
        <l:layout norefresh="true">
                <st:include it="${it.owner}" page="sidepanel.jelly" />
                <l:main-panel>
(contenu principal ici)
                </l:main-panel>
        </l:layout>
</j:jelly>

Le getOwner() mentionné auparavant est ici utilisé via ${it.owner} afin de lier à la construction et afficher la colonne de gauche standard.

Les méthodes de la forme getVar() de TestResult peuvent être accédées via ${it.var}, et ainsi de suite.

Sixième étape : du paramétrage

Un plugin a généralement besoin de paramètres, et évidemment un système est prévu pour cela dans Jenkins.

Il faut commencer par créer un fichier src/main/resources/(package)/TestRecorder/config.jelly, qui contiendra :

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <f:entry title="Parameter" description="This is a test parameter">
    <f:textbox name="param" value="${instance.param}" />
  </f:entry>
</j:jelly>

Explications :
- un layout standard, avec une zone de saisie simple ligne
- la variable sera nommée « param »
- il est possible de rajouter un bloc « <f:advanced> », qui sera affiché caché avec un bouton « avancé »
- de nombreux autres types existent, voir les documentations ou des plugins existants

Pour faire le lien avec le TestRecorder, trois choses à faire :
- annoter le constructeur avec @DataBoundConstructor
- ajouter au constructeur un paramètre String param
- ajouter une méthode String getParam() (conventions standard Java)

Et la valeur devrait être gérée automatiquement par Jenkins.

Astuces pour finir

En vrac :
- pour récupérer une construction précédente (par exemple pour afficher des comparaisons), on peut à partir d’un AbstractBuild (sérialisé ou non) utiliser getPreviousBuild()
- depuis une construction, il est possible d’utiliser getAction(TestAction.class) pour récupérer l’action associée et retrouver nos objets
- il est possible de changer le port d’exécution de Jenkins (utile quand on a déjà une instance habituelle en cours d’exécution), dans NetBeans faire bouton droit, « propriétés », rubrique « actions » [1], choisir l’action « run project » et ajouter une ligne jetty.port=5555. Il est possible de faire de même pour le débogage.

Conclusions

J’admets avoir eu un peu de mal avec les documentations, qui me semblent parfois assez peu fournies.

Il manque des tutoriaux simples genre « pour faire telle chose : faire 1, 2 et 3 ».

La documentation de l’API Jenkins elle-même est bien fournie, et donne de nombreux exemples (au hasard la page sur Action, qui mentionne floatingBox, etc.).

Un autre point négatif à mon sens est que Jelly [2] utilise la réflexion pour accéder aux champs, et on perd donc le typage fort de Java ainsi que les vérifications lors de la compilation.

Au final une expérience intéressante et instructive, même s’il m’a fallu pas mal tâtonner et chercher sur Internet.


[1Qui apparaît chez moi comme « [fuzzy] Actio&ns », sans doute un souci de traduction.

[2Mais JSP souffre du même souci.