Hudson. Parte 2. Crea tus propios plugins

Una de las ventajas de Hudson es la posibilidad que nos ofrece de ampliar su funcionalidad mediante su sistema de plugins. De esta forma, podemos realizar acciones sobre los builds, tales como generación de informes, controladores, acciones sobre el código fuente, etc. En Internet podemos encontrar algunas páginas de donde descargar plugins para Hudson, como por ejemplo http://hudson.gotdns.com/wiki/display/HUDSON/Plugins.

Aún disponiendo de una cantidad considerable de plugins en la red que cubran la mayoría de las necesidades, nos podemos encontrar en la situación de tener que crear uno propio. Si te has planteado desarrollar uno te habrás dado cuenta de que la documentación en Internet sobre cómo crear un plugin es muy escasa, estando el grueso de la información en sólo dos páginas: http://hudson.gotdns.com/wiki/display/HUDSON/Plugin+tutorial y http://javaadventure.blogspot.com/2008/01/writing-hudson-plug-in-part-1.html.

Pues bien, en Viavansi surgió la necesidad de crear un plugin para Hudson que subiera el código fuente de un proyecto a un repositorio de Subversion, de forma que se automatizara esta tarea cada vez que Hudson hiciera un build.

El plugin constará de una parte de configuración en donde el usuario encargado de cada proyecto introducirá la url del repositorio de Subversion donde desea subir el código, los parámetros relativos al repositorio (nombre de usuario y password) así como dos entradas para ficheros que no se quieren subir o ficheros que no se quieren sobreescribir en el destino. Además dispondrá de una opción para habilitar o deshabilitar el plugin en todos los proyectos de forma global.

Creación de la estructura del plugin

Lo primero que vamos a tener que usar para crear el plugin es la herramienta Maven, la cual nos creará la estructura de paquetes, además de algunos ficheros de ejemplos. Esto era de esperar, pues Hudson internamente usa Maven para la creación de los builds de los proyectos.

Empezamos situandonos en una consola sobre nuestro directorio de proyectos y tecleamos:

mvn hpi:create

Después de pedirnos algunos datos como el identificador del grupo y el nombre del plugin, Maven nos creará la siguiente estructura:

+ src

+ main

+ java

+ groupid.name

+- PluginNamePublisher.java

+- PluginImpl.java

+ resources

+ groupid.name.project

+- config.jelly

+- global.jelly

+- index.jelly

+ webapp

+- help-globalConfig.html

+- help-projectConfig.html

Por defecto, se crean unos ficheros de ejemplo (el archiconocido HelloWorld) que podemos usar para ver cómo está implementado un plugin sencillo.

Si queremos tener soporte en nuestro IDE para la implementación del plugin deberemos, en el caso de Eclipse, tendremos que introducir lo siguiente:

mvn -DdownloadSources=true eclipse:eclipse

Si tenemos otros entornos como Netbeans o IntelliJ, existen plugins específicos de Maven para estos IDEs.

Bien, acabamos de crear la estructura de nuestro primer plugin, nos falta decirle a Hudson qué tipo de plugin hemos desarrollado, agregar la lógica y crear los campos visuales para que se añadan a las páginas de configuración de los proyectos.

Creando el plugin

Uno de los aspectos más importantes en la creación de un plugin para Hudson consiste en determinar el tipo de proyecto para el que va a ser utilizado. En nuestro caso utilizamos proyectos de tipo Maven, por lo cual necesitamos publicar el plugin en Hudson a través de un Publisher; en caso contrario (para proyectos de tipo Ant, por ejemplo) necesitaríamos publicarlo usando un Builder. Otra de las características de usar un Publisher frente a un Builder consiste en que la acción se ejecuta cuando un proyecto ha terminado de hacer el build.

Para publicar el plugin mediante un Publisher basta con tocar la clase PlugImp quedando de la siguiente forma:

public class PluginImpl extends Plugin {

public void start() throws Exception {

//Publicamos el descriptor del plugin

Publisher.PUBLISHERS.add(SVNCopyPublisher.DESCRIPTOR);

}

}

En este código podemos ver como se ha modificado la clase inicialmente creada por Maven, añadiendo la línea donde le decimos a Hudson que registre nuestro plugin. Debido a que la funcionalidad que queremos desarrollar consiste en copiar el código fuente de un proyecto a un repositorio de Subversion, el nombre de nuestro plugin será SVNCopyPlugin (original, verdad?), creando así la clase SVNCopyPublisher que será la que contenga el descriptor y la lógica de negocio.

Los elementos más importantes de la clase SVNCopyPublisher son:

  • Atributos necesarios para la ejecución de la lógica del plugin. En nuestro caso, url del repositorio, nombre de usuario, contraseña y listas con los ficheros que no queremos subir o que no queremos sobreescribir. Cada atributo deberá tener un método público get y set.
  • Un constructor público en el que pasaremos los valores de los campos rellenados por el usuario en la configuración del plugin e inicializaremos los atributos definidos en el punto anterior. Además, este constructor deberá estar anotado con @DataBoundConstructor.

  • Un método perform en donde se colocará la lógica de nuestro plugin.

  • Un atributo estático y final llamado DESCRIPTOR que será instancia de una clase interna de nombre DescriptorImpl.

A su vez, la clase DescriptorImpl contendrá la configuración global del plugin. En el caso de SVNCopyPlugin hemos usado en la configuración local un checkbox con el cual activaremos el uso del plugin y sólo realizará la copia cuando esté activado.

Además esta clase debe contener:

  • Los atributos propios de la configuración global del plugin, con sus gets y sets.

  • Un constructor protected sin parámetros.

  • El método configure que hará persistente la configuración global.

  • Un método newInstance que devolverá una instancia de nuestra clase Publisher con los valores introducidos por el usuario.

Hasta aquí ha sido todo teoría, veamos ahora como queda la clase SVNCopyPublisher y su clase interna DescriptorImpl:

public class SVNCopyPublisher extends Publisher {

private Boolean enable;

private final String urlTarget, nameTarget, passTarget, filesTargetException, filesSourceException;

@DataBoundConstructor

public SVNCopyPublisher(String urlTarget, String nameTarget, String passTarget, String filesSourceException, String filesTargetException) {

this.urlTarget = urlTarget;

this.nameTarget = nameTarget;

this.passTarget = passTarget;

this.filesTargetException = filesTargetException;

this.filesSourceException = filesSourceException;

}

//GETTERS y SETTERS de los atributos

public boolean needsToRunAfterFinalized() {

return true;

}

/*

* Contiene la lógica del plugin, que se ejecutará cuando termine el build de Hudson

*/

@Override

public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {

if(enable && build.getResult().equals(Result.SUCCESS)){

//Aquí debe ir la lógica de negocio del plugin

}

return true;

}

public Descriptor<Publisher> getDescriptor() {

return DESCRIPTOR;

}

public Action getProjectAction(AbstractProject< ?, ?> project) {

return null;

}

/**

* Descriptor should be singleton.

*/

public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();

/**

* Descriptor for {@link SVNCopyPublisher}. Used as a singleton.

* The class is marked as public so that it can be accessed from views.

*/

public static final class DescriptorImpl extends Descriptor<Publisher> {

/**

* If you don’t want fields to be persisted, use transient

*/

private boolean enable;

protected DescriptorImpl() {

super(SVNCopyPublisher.class);

load();

}

/**

* This human readable name is used in the configuration screen.

*/

public String getDisplayName() {

return “SVNCopy Plugin Project Configuration”;

}

/**

* Get the fields from the global configuration form and persist them.

*/

public boolean configure(StaplerRequest req, JSONObject o ) throws FormException {

enable = o.getBoolean(“enable”);

save();

return super.configure(req,o);

}

/**

* Creates a new instance of {@link SVNCopyPublisher} from a submitted form.

*/

@Override

public SVNCopyPublisher newInstance(StaplerRequest req, JSONObject formData) throws FormException {

SVNCopyPublisher pub = req.bindJSON(SVNCopyPublisher.class, formData);

pub.setEnable(enable);

return pub;

}

public boolean isEnable(){

return enable;

}

}

}

Del código de arriba podemos destacar el método perform, que es el encargado de llamar a la lógica del plugin, recibiendo como parámetros una instancia de AbstractBuild (usado para lanzar el plugin si el build ha terminado satisfactoriamente), una de Launcher que contiene datos de la máquina que realiza el build y otra de Listener, que entre otras cosas contiene el logger con el cual podemos imprimir en la consola web de Hudson nuestra salida del plugin. Otros métodos importantes son configure y newInstance que reciben como parámetros la petición del formulario de configuración y los datos introducidos por el usuario en forma de JSON. El parámetro StaplerRequest forma parte del framework Stapler utilizado por Hudson para la aplicación web y es útil si queremos obtener datos de la petición usando getParameter.

Generando los formularios y uniéndolos al código del plugin

Para terminar con el desarrollo de nuestro plugin nos falta crear los formularios de entrada en la parte de configuración global de Hudson, así como en la configuración particular de cada proyecto. Maven nos crea por defecto los ficheros global.jelly y config.jelly, además de unos htmls de ayuda. Estos ficheros siguen un esquema XML basandose en un framework ligero llamado Jelly http://commons.apache.org/jelly/.

Para comenzar, tenemos que incluir en la configuración global de Hudson el checkbox que habilitará de forma general el uso del plugin para todos los builds que se hagan (además se dispone de otro checkbox que habilita el plugin para cada proyecto). Dentro de la carpeta resources se encontrarán como mínimo tres ficheros .jelly: index, global y config. El primer fichero contiene una descripción del plugin y en los otros dos se deberá colocar los elementos htmls para la configuración global y específica del mismo.

En el fichero global.jelly queremos colocar el checkbox que hará que se active el plugin para todos los proyectos. Para ellos basta con colocar las siguientes lineas:

<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:section title=”SVNCopyPlugin Global Configuration”>

<f:entry title=”Enable”

description=”Check if you want to perform a copy of your project from your repository to another one”>

<f:checkbox name=”enable” checked=”${descriptor.isEnable()}” />

</f:entry>

</f:section>

</j:jelly>

Como se puede apreciar el código es bastante autoexplicativo. Se usan unos tags propios de Jelly para añadir el checkbox a la configuración de Hudson (Manage Hudson / Configure System). El valor del checkbox estará bindeado al atributo enable ya visto en el descriptor.

 Configuración global del plugin

Por otra parte el fichero config.jelly contendrá la configuración específica del plugin para cada proyecto. Su contenido será el siguiente:

<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”>

<!–

Cajas de texto para introducir las url de los dos repositorios, los nombres y los passwords

–>

<f:entry title=”URL del repositorio destino”>

<f:textbox field=”urlTarget” value=”${descriptor.urlTarget}”/>

</f:entry>

<f:entry title=”Nombre de usuario del repositorio destino”>

<f:textbox field=”nameTarget” value=”${descriptor.nameTarget}”/>

</f:entry>

<f:entry title=”Password del repositorio destino”>

<f:password field=”passTarget” value=”${descriptor.passTarget}”/>

</f:entry>

<f:entry title=”Ficheros y carpetas que no se desean sobreescribir en el destino”>

<f:textbox field=”filesTargetException” value=”${descriptor.filesTargetException}”/>

</f:entry>

<f:entry title=”Ficheros y carpetas que no se desean subir al destino”>

<f:textbox field=”filesSourceException” value=”${descriptor.filesSourceException}”/>

</f:entry>

</j:jelly>

Gracias a este fichero se incluirá en la configuración del proyecto las cajas de texto para que cada usuario introduzca sus datos sobre el repositorio. Cada elemento f:textbox leerá/escribirá su valor en un atributo del descriptor.

 Configuración del plugin para cada proyecto

Ejecución y debug

Con todo esto ya hemos terminado de programar el plugin, ahora sólo queda ejecutarlo y ver que todo va como deseamos. Para ello usaremos el comando mvn hpi:run desde la carpeta del plugin, el cual nos montará un Hudson en nuestra máquina y con el que podremos trastear antes de instalarlo en el Hudson de la empresa.

También es interesante la opción de debuguear para seguir la traza y ver qué valores van tomando cada uno de los elementos que intervienen en la ejecución del plugin. Para tener la posibilidad de debuguear antes de llamar a mvn hpi:run tendremos que setear las variables de entorno con los comandos:

export MAVEN_OPTS=”-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n”

si usamos Unix o

set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n

si usamos Windows.

Empaquetando y exportando el plugin

Para finalizar sólo queda crear el fichero .hpi para importarlo desde el Hudson instalado en uno de los servidores de la empresa, algo tan sencillo como ejecutar mvn hpi:package y coger el fichero con esta extensión creado en la carpeta target.

Con esto ya tienes para empezar a customizar tu Hudson añadiendole la funcionalidad que necesitas y no te la dan otros plugins. Ya se sabe, si no está: hazlo tu mismo.

Próximamente…

En breve pondremos una entrada explicando cómo sacarle más partido a los plugin de Hudson, añadiendo interacción con el usuario, generación de informes del plugin por cada build, etc.

Comentarios

  1. Muy bueno, muy buena explicación sobre el proceso de creación de plugins! Te ha faltado ponerle unas pastas duras, un código ISBN :p.

  2. Pingback: Xnoccio - » Hudson. Parte 1 - Introducción.
  3. Pingback: Xnoccio.com - » Hudson: Nuestro compañero

Comments are closed.