Plugins Hudson – Episode 2 : Implémenter son premier plugin


Suite de ma série de billets consacrés au développement de plugin Hudson : aujourd’hui, nous allons parler développement et mettre davantage les mains dans le cambouis que dans l’épisode 1.

Nous allons voir notamment comment implémenter des points d’extension ainsi qu’une vue et un traitement coté serveur en utilisant les technologies Jelly et Stapler

Comment et où poser des points d’extension

Un plugin Hudson est en fait un regroupement de points d’extension sur votre instance d’Hudson. Un point d’extension étant une classe java généralement abstraite qu’il suffit de sous-classer afin de rajouter du comportement à Hudson.

La mise en place d’un plugin Hudson se fait généralement en trois temps :

  • Tout d’abord, il est nécessaire de créer une classe java qui sera le squelette du Plugin, et qui portera l’ensemble des informations de ce dernier (données persistées notamment… nous y reviendrons dans un épisode ultérieur !). Pour le plugin global-build-stats, j’ai décidé d’hériter de la classe hudson.Plugin, en revanche il est également possible de sous-classer d’autres arborescences de classes (notamment l’arborescence Builder permettant de rajouter des paramètres à un Build).
  • Ensuite il faudra aller déterminer les points d’extension que vous souhaitez cibler dans votre plugin.
    Pour cela, il faut se rendre (encore une fois) sur le wiki sur la page qui liste l’ensemble des points d’extensions existants dans Hudson.  Comme vous pourrez le constater, il y en a un nombre assez conséquent (plus de 60 au moment où j’écris ces lignes) !
  • Enfin, une fois les points d’extension identifiés, il sera nécessaire de sous-classer chacun d’eux par une classe java et de les référencer via l’annotation @Extension

Là encore, je ne saurais que trop vous conseiller de jeter un oeil à des plugins qui implémentent déjà les points d’extension que vous ciblez, afin d’avoir un exemple d’implémentation de ce dernier (la documentation existante se résume malheureusement à la javadoc qui, généralement, n’est pas très compréhensible pour un néophyte).

Un exemple concret : pour être capable de déclencher un traitement à la fin de chaque Build (pour mettre à jour les statistiques du global-build-stats plugin en fonction du résultat du Build par exemple), il m’a suffit de rajouter un point d’extension sur le RunListener comme ceci :

// Déclaration du plugin GlobalBuildStats
public class GlobalBuildStatsPlugin extends Plugin {
    //...
    // Déclaration d'un point d'extension sur le comportement d'Hudson
    @Extension
    public static class GlobalBuildStatsRunListener extends RunListener<AbstractBuild>{
    	public GlobalBuildStatsRunListener() {
    		super(AbstractBuild.class);
		}

    	@Override
    	public void onCompleted(AbstractBuild b, TaskListener listener) {
    		super.onCompleted(b, listener);

    		// Affichons un Hello world dans la console a la fin de chaque job !
    		System.out.println("Hello world !");
    		System.out.println("Build result : "+b.getResult());
    	}
    }
}

En lançant Hudson (cf episode 1), vous pourrez vous apercevoir qu’à la fin de chacun de vos builds, le résultat de ce dernier sera affiché dans la console.

La première vue

Nous ne rentrerons pas dans les détails de Jelly et Stapler dans ces épisodes (je me considère comme débutant sur ces technos !), en revanche, la plupart des plugins étant basés sur une IHM, nous allons voir le minimum vital pour mettre en place quelque chose de fonctionnel rapidement.

Pendant les développements du plugin global-build-stats, j’ai décidé de rendre disponible une vue spécifique au plugin, à travers le panel d’administration.
Je connaissais déjà bien le plugin disk-usage qui rajoutait un lien sur le panel d’administration afin de gérer l’occupation filesystem dûe aux différents jobs/workspaces d’hudson. J’ai donc réalisé un checkout des sources de ce plugin et me suis aperçu très rapidement que pour ce faire, il était nécessaire de surcharger le point d’extension LinkManagement.
J’ai donc réalisé cette surcharge de la manière suivante :

    @Extension
    public static class GlobalBuildStatsManagementLink extends ManagementLink {

        public String getIconFileName() {
            return "/plugin/global-build-stats/icons/global-build-stats.png";
        }

        public String getDisplayName() {
            return "Global Builds Stats";
        }

        public String getUrlName() {
            return "plugin/global-build-stats/";
        }

        @Override public String getDescription() {
            return "Displays stats about daily build failures";
        }
    }

Note: Dans l’exemple ci-dessus, rien n’est internationalisé : c’est mal !.. Nous reviendrons sur ce sujet dans un prochain épisode !

Ce point d’extension permet donc de rajouter un lien dans le panel d’administration qui, lorsqu’il est cliqué, renvoit vers l’url http://${HUDSON_ROOT}/plugin/global-build-stats/

Lorsque cette URL est appelée, un script Jelly est recherché à cet emplacement : src/main/resources/${FQN de votre plugin}/index.jelly.
Dans le cas du plugin global build stats, il s’agit de src/main/resources/hudson/plugins/global_build_stats/GlobalBuildStatsPlugin/index.jelly

Pour rester dans quelque chose de simple, j’ai tout d’abord souhaité générer une page pas du tout dynamique (version 0.1-alpha1 du global-build-stats) qui pointait simplement sur une URL de mon plugin dont le but était de générer une image.
Pour ce faire, le script jelly suivant fait l’affaire :

<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">
<!-- Global layout title here -->
<l:layout title="Global Build Stats" secured="true">

  <l:side-panel>
    <l:tasks>
      <!-- Left side panel link "back to dashboard" -->
      <l:task icon="images/24x24/up.gif" href="${rootURL}/" title="Back to Dashboard" />
    </l:tasks>
  </l:side-panel>

  <l:main-panel>
    <!-- Page title -->
    <h1><img src="${rootURL}/plugin/global-build-stats/icons/global-build-stats.png" /> Global Build Stats </h1>

        <!-- Global-build-stats chart display -->
	<img src="${rootURL}/plugin/global-build-stats/createChart?title=Build counts&amp;buildStatWidth=650&amp;buildStatHeight=400&amp;historicLength=30&amp;historicScale=DAILY&amp;successShown=true&amp;failuresShown=true&amp;unstablesShown=true&amp;abortedShown=true&amp;notBuildsShown=true&amp;jobFilter=ALL" /><br/>

  </l:main-panel>
</l:layout>
</j:jelly>

Quelques commentaires sur ce premier script Jelly :

  • Un script Jelly est un fichier XML qui doit être syntaxiquement valide
  • Il est possible de faire appel à des « taglibs » (notion similaire à celle utilisée dans les JSPs) en définissant des namespaces dans la balise racine j:jelly. Les taglibs disponibles ressemblent beaucoup aux taglibs « standard » des JSP (cf listes des taglibs Jelly disponibles : core, define, stapler et hudson)
  • Il ne me semble pas que du scripting (dans un langage autre que Jelly) soit possible dans un script Jelly : l’équivalent des scriptlets JSP n’est pas faisable … ça peut être un bien (on ne mêle pas un langage descriptif (XML/HTML) avec un langage procédural (Java)) comme ça peut entraîner une lourdeur (if/then/else assez verbeux avec j:choose/j:when/j:otherwise)
  • Une syntaxe très proche des ELs (il s’agit en fait d’un sur-ensemble des EL … malheureusement pas du tout standardisé comme le souhaiteraient les militants d’une JSR pour les EL) peut être utilisée (cf ${rootUrl} qui permet d’accéder à l’url d’Hudson en absolu). Les scopes request/session/page/application sont accessibles (ex: ${requestScope[‘param’]})
    • Une remarque à ce sujet : il est fort dommage qu’il n’y ait pas (je ne l’ai pas trouvée sur le wiki en tous les cas !) de documentation associée aux variables globales, comme rootUrl, accessibles depuis n’importe quel script Jelly. Pour pouvoir être capable d’intuiter les différentes variables existantes dans le contexte Jelly, j’ai dû faire un gros coup de debugger pendant 1 à 2h afin de m’apercevoir que les noms des différents scopes étaient les mêmes que ceux des JSP.
  • Les ressources (exemple ${rootURL}/plugin/global-build-stats/icons/global-build-stats.png) doivent être positionnées dans le src/main/webapp/ du plugin (exemple : src/main/webapp/icons/global-build-stats.png)
  • L’url appelée par l’image à afficher est ${rootURL}/plugin/global-build-stats/createChart : nous allons voir comment la mapper dans notre classe de plugin

Le rendering de l’image via le plugin

Le framework Stapler est un framework REST-like qui va :

  • Tenter de résoudre le contrôleur ciblé par l’url courante (identification de la classe GlobalBuildStatsPlugin à partir de l’url http://${HUDSON_ROOT}/plugin/global-build-stats/)
  • Tenter de résoudre la méthode à appeler sur le contrôleur, toujours à partir de l’url : appel de la méthode doCreateChart(StaplerRequest,StaplerResponse)

Si bien qu’avec le code ci-dessous, nous générons un graphique dont le titre est « Hello world » et dont les dimensions sont passées en GET dans l’url :

public class GlobalBuildStatsPlugin extends Plugin {
    // ....
    // Handler will be called on the /plugin/global-build-stats/createChart url
    public void doCreateChart(StaplerRequest req, StaplerResponse res) throws ServletException, IOException {
        ChartUtil.generateGraph(req, res, createChart(req),
        		Integer.valueOf(req.getParameter("buildStatWidth")), Integer.valueOf(req.getParameter("buildStatHeight")));
    }

    private JFreeChart createChart(StaplerRequest req) {
    	final JFreeChart chart = ChartFactory.createStackedAreaChart("Hello world !",
    			null, "Count",
    			// Yup ! Our dataset is empty for the moment !
    			new DataSetBuilder<String, String>().build(),
    			PlotOrientation.VERTICAL, true, true, false);
        chart.setBackgroundPaint(Color.white);

        return chart;
    }
    // ....
}

Conclusion

Suite à cet article, vous devriez être capable d’implémenter un plugin basique qui n’aurait ni recours à de la persistance d’informations, ni à une IHM évoluée (aucune soumission de données par l’utilisateur).

Nous avons pu voir qu’Hudson était basé sur deux technologies peu connues des développeurs J2EE :

  • Jelly (projet Apache) : technologie de scripting utilisée coté présentation, à fonctionnalités plus ou moins équivalentes aux JSP (pour le moment, je n’ai pas encore trouvé de plu-value par rapport aux JSP) mais par contre beaucoup moins répandue (peu de développeur connaissent sa syntaxe et son fonctionnement, les IDE n’ont pas forcément de complétion très évoluée à son sujet …). Cette technologie avait été utilisée pour les développements de plugins Maven 1 (à l’époque) et rapidement laissée de coté dans Maven 2 au profit de classes Java annotées.
  • Stapler : Framework MVC Rest-like, initié par Kohsuke Kawaguchi (papa de Hudson, qui a récemment annoncé son départ de Sun pour se consacrer à Hudson), lui aussi extrêmement peu connu

On peut s’interroger quand à ces choix technologiques qui se placent en décalage par rapport à la philosophie d’ouverture d’Hudson souhaitée à travers l’API des Plugins.
Peut-être est-ce là une volonté assumée visant à vouloir faire connaître ces deux technologies, particulièrement Stapler dont Kohsuke est l’initiateur. Peut-être aussi est-ce dû au « code legacy » du projet qui n’était pas forcément voué à un tel succès, lors de sont lancement il y a de ça 3 ans…

Dans un prochain billet, nous traiterons des aspects avancés de la couche présentation (soumission de formulaire, validation de ces derniers, comportements javascript, limitations) ainsi que les moyens mis à disposition pour persister les données.

Références du billet

Episode 1 : https://fcamblor.wordpress.com/2010/04/04/plugins-hudson-etape-1-la-creation-du-plugin/
Liste des points d’extension d’Hudson :  http://wiki.hudson-ci.org/display/HUDSON/Extension+points
Les différents pointeurs du wiki pour le développement de plugins : http://wiki.hudson-ci.org/display/HUDSON/Extend+Hudson
Des pointeurs sur les taglibs Jelly : coredefinestapler et hudson
Le projet Jelly : http://commons.apache.org/jelly/
Le projet Stapler : https://stapler.dev.java.net/what-is.html
Article proposant la création d’une JSR permettant d’unifier les différent EL : http://relation.to/14410.lace

Publicités
Publié dans Hudson, Maven. 4 Comments »

4 Réponses to “Plugins Hudson – Episode 2 : Implémenter son premier plugin”

  1. Fabien Says:

    Super initiative, des tuto fr sur ce sujet sont rares!
    A quand la suite? 🙂

  2. Plugins Hudson – Episode 3 : Des formulaires et des données « Frédéric Camblor Dev Blog Says:

    […] au développement de plugins Hudson. Dans cet épisode qui se situe dans le prolongement de l’implémentation de son premier plugin, je vais principalement traiter de  la gestion des données, depuis leur saisie par un utilisateur […]


Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :