Extendiendo sonarqube con un plugin personalizado

2
7768

En este tutorial veremos cómo extender sonarqube, la herramienta open source más conocida para
medir la calidad del código de nuestros proyectos, con un plugin personalizado.

Extendiendo sonarqube con un plugin personalizado.

 

0. Índice de contenidos.

1. Introducción

Ya hemos hablado en otros tutoriales sobre cómo extender sonarqube para, por ejemplo,

añadir nuestras propias reglas haciendo uso de xpath
.

En este tutorial vamos a ir un paso más allá, creando nuestro propio plugin.

Los puntos de extensión sobre los que podemos trabajar son básicamente los siguientes:

  • añadir reglas de código,
  • añadir eventos para notificar de los resultados de análisis a aplicaciones externas,
  • añadir métricas,
  • añadir el soporte de nuevos lenguajes de programación,
  • añadir el soporte de sistemas de control de versiones,
  • añadir proveedores de autenticación,
  • extender con widgets y traducir la interfaz de usuario.

A todo lo anterior podemos añadir que siempre existe la posibilidad de hacer uso del API REST
para desarrollar nuestras propias aplicaciones, fuera del entorno propiamente dicho de sonarqube.

El objetivo de este tutorial es examinar las posibilidades que tenemos para la creación de un plugin propio,
que se instale como tal en sonarqube y que permita incluir en la interfaz de usuario un widget que
explote la información de las métricas de un proyecto.

Y si a alguien le sabe a poco, además vamos a trabajar con un sonarqube dockerizado, para que
montarnos el entorno de desarrollo y desplegar el plugin sea sencillo.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS El Capitan 10.11
  • Sonarqube 5.6
  • Docker 1.11.2.

3. Preparación del entorno de desarrollo.

Como comentábamos en la intro vamos a trabajar con una imagen de docker para desplegar sonarqube y
hacer nuestras pruebas de despliegue del plugin; en realidad vamos a trabajar con docker-compose que
nos permite configurar y desplegar en una sola acción más de un contenedor de docker ya que,
en nuestro caso, necesitamos una base de datos, no queremos trabajar con la embebida y la
propia aplicación de sonarqube.

Para ello, lo primero es crearnos un fichero docker-compose.yml con el siguiente
contenido que luego ubicaremos en la raíz de nuestro proyecto.

  version: '2'
  services:
    sonarqube:
      build:
        context: .
      image: sonarqube:5.6
      ports:
        - 9000:9000
      links:
        - db:mysql
      networks:
        - sonarnet
      environment:
        - SONARQUBE_JDBC_URL=jdbc:mysql://mysql:3306/sonarqube?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
        - SONARQUBE_JDBC_USERNAME=sonarqube
        - SONARQUBE_JDBC_PASSWORD=sonarqube
     volumes:
        - ./src/main/conf/:/opt/sonarqube/conf/

    db:
      image: mysql:5.6
      hostname: mysql
      ports:
        - 3306:3306
      networks:
        - sonarnet
      environment:
        - MYSQL_ROOT_PASSWORD=root
        - MYSQL_USER=sonarqube
        - MYSQL_PASSWORD=sonarqube
        - MYSQL_DATABASE=sonarqube

  networks:
    sonarnet:
      driver: bridge

Estamos usando la sintaxis de la última versión de docker con dos contenedores basados en las imágenes de sonar:5.6 y mysql:5.6;
la de sonar depende de la de mysql y ambas se despliegan en una red creada ad hoc.

La parte de «volumes» del contenedor de sonarqube nos permite configurar un volumen de datos y
«exponer» un directorio de nuestra máquina hacia el contenedor, de modo que para nuestros propósitos podemos mantener
los ficheros de configuración de sonar dentro de nuestro propio proyecto para configurarlos de forma externa al contenedor
y distribuirlos con el propio entorno de desarrollo.

Por último, haremos uso de un par de instrucciones dentro de un fichero Dockerfile para que,
crear una imagen nueva copiando el plugin generado al directorio de plugins del contenedor y así poder probar
nuestro desarrollo.

FROM sonarqube
ADD  ./target/sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar /opt/sonarqube/extensions/plugins/sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar

En este último punto nos estamos adelantando a la creación del proyecto del plugin propiamente dicho, pero es justo lo que vamos a
ver a continuación.

4. Creación del proyecto.

Sonarqube proporciona soporte para crear un proyecto de plugin basado en maven, para ello no tenemos más que crearnos un proyecto java simple,
haciendo uso de arquetipo maven-archetype-quickstart y sustituir el pom.xml por uno como el siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.autentia.sonar.plugins</groupId>
	<artifactId>sonar-tnt-audit-plugin</artifactId>
	<packaging>sonar-plugin</packaging>
	<version>0.0.1-SNAPSHOT</version>

	<name>SonarQube Autentia audit :: Plugin</name>
	<description>Autentia audit plugin for SonarQube</description>

	<organization>
		<name>Autentia</name>
		<url>http://www.autentia.com</url>
	</organization>
	<licenses>
		<license>
			<name>GNU LGPL 3</name>
			<url>http://www.gnu.org/licenses/lgpl.txt</url>
			<distribution>repo</distribution>
		</license>
	</licenses>

	<properties>
		<sonar.pluginName>Autentia audit</sonar.pluginName>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<sonar.apiVersion>5.6</sonar.apiVersion>
		<jdk.min.version>1.8</jdk.min.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.sonarsource.sonarqube</groupId>
			<artifactId>sonar-plugin-api</artifactId>
			<version>${sonar.apiVersion}</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.sonarsource.sonarqube</groupId>
			<artifactId>sonar-ws</artifactId>
			<version>${sonar.apiVersion}</version>
		</dependency>
		<dependency>
			<!-- packaged with the plugin -->
			<groupId>commons-lang</groupId>
			<artifactId>commons-lang</artifactId>
			<version>2.6</version>
		</dependency>


		<!-- unit tests -->
		<dependency>
			<groupId>org.sonarsource.sonarqube</groupId>
			<artifactId>sonar-testing-harness</artifactId>
			<version>${sonar.apiVersion}</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.11</version>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
				<artifactId>sonar-packaging-maven-plugin</artifactId>
				<version>1.16</version>
				<extensions>true</extensions>
				<configuration>
					<pluginClass>com.autentia.sonar.plugins.AuditPlugin</pluginClass>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.5.1</version>
				<configuration>
					<source>${jdk.min.version}</source>
					<target>${jdk.min.version}</target>
				</configuration>
			</plugin>
			<plugin>
				<!-- UTF-8 bundles are not supported by Java, so they must be converted
					during build -->
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>native2ascii-maven-plugin</artifactId>
				<version>1.0-beta-1</version>
				<executions>
					<execution>
						<goals>
							<goal>native2ascii</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</project>

En el plugin de empaquetación debemos indicar la ruta y nombre de una clase que hará el
registro de nuestros componentes del plugin en los distintos contextos.

<pluginClass>com.autentia.sonar.plugins.AuditPlugin</pluginClass>

Con el soporte del plugin de maven de sonarqube, se empaquetará un jar preparado para ser
desplegado en sonarqube, justo el artefacto que configurábamos en el punto anterior para
copiar en el directorio de plugins del contenedor en el Dockerfile.

Solo nos falta un paso más, opcional, si queremos configurar sonar en modo desarrollo podemos extraer el fichero
sonar.properties del contenedor y ubicarlo en un directorio de conf que será el que se monte como volumen en el contenedor:

Si ejecutamos la siguiente secuencia de comandos, se levantarán los dos contenedores en segundo plano:

  mvn package
  docker-compose up -d

Como comentaba, podemos copiarnos el fichero de propiedades haciendo uso del siguiente comando:

docker cp sonarqube:/opt/sonarqube/conf/sonar.properties ./conf/sonar.properties

En el mismo podríamos modificar la siguiente variable

sonar.web.dev=true

y reiniciar el contenedor para que tome en caliente los cambios

  docker-compose restart sonarqube

Al arrancar docker, si utilizamos las tools, nos informa de la ip asociada a la máquina virtual:

                        ##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
           \______ o           __/
             \    \         __/
              \____\_______/


docker is configured to use the default machine with IP 192.168.99.100
For help getting started, check out the docs at https://docs.docker.com

Si accedemos desde un navegador a sonarqube, en mi caso http://192.168.99.100:9000,
entramos con un usuario administrador (admin:admin) al update center

sonarqube-custom-plugin-01

Podríamos comprobar que el plugin se encuentra instalado

sonarqube-custom-plugin-02

Si tenemos problemas con la copia del artefacto, del jar, podemos entrar en el contenedor:

docker exec -it sonartntauditplugin_sonarqube_1 bash

Para comprobar que el jar está en la ubicación correcta:

  root@0c5f7cf0a83a:/opt/sonarqube/extensions/plugins# pwd
  /opt/sonarqube/extensions/plugins
  root@0c5f7cf0a83a:/opt/sonarqube/extensions/plugins# ls -la
  total 26816
  drwxr-xr-x 2 root root    4096 Jul 12 07:22 .
  drwxr-xr-x 6 root root    4096 Jul  8 15:00 ..
  -rw-r--r-- 1 root root     128 Apr 11 09:58 README.txt
  -rw-r--r-- 1 root root 7797781 Apr  7 15:23 sonar-csharp-plugin-5.0.jar
  -rw-r--r-- 1 root root 3191477 Apr 28 08:44 sonar-java-plugin-3.13.1.jar
  -rw-r--r-- 1 root root 1678073 Apr  7 15:23 sonar-javascript-plugin-2.11.jar
  -rw-r--r-- 1 root root 3233128 Apr  7 15:23 sonar-scm-git-plugin-1.2.jar
  -rw-r--r-- 1 root root 6564535 Apr  7 15:23 sonar-scm-svn-plugin-1.3.jar
  -rw-r--r-- 1 root root 4970028 Jul 12 07:26 sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar
  root@0c5f7cf0a83a:/opt/sonarqube/extensions/plugins#

Si no se encontrarse, siempre podemos ejecutar una copia manual desde el contenedor, ejecutando el siguiente comando:

docker cp target/sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar sonartntauditplugin_sonarqube_1:/opt/sonarqube/extensions/plugins/sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar

Si tenemos sonar en modo desarrollo se recargará, sino podemos recargar el contenedor manualmente

docker-compose restart sonarqube

El modo de trabajo a partir de este punto será, modificar el código del proyecto, compilar y empaquetar con el soporte de maven:

mvn clean package

Después, hacer una copia del artefacto generado:

docker cp target/sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar sonartntauditplugin_sonarqube_1:/opt/sonarqube/extensions/plugins/sonar-tnt-audit-plugin-0.0.1-SNAPSHOT.jar

y esperar a que se redespliegue o hacer un reinicio manual

docker-compose restart sonarqube

También podríamos montar un volumen externo que apunte al directorio de plugins fuera, en vez de hacer copias del fichero; sería otra opción.

5. Explorando las distintas opciones.

Una vez disponemos de un entorno de desarrollo y con el objetivo en mente de crear nuestro plugin
debemos ver qué tipo de funcionalidad vamos a implementar puesto que en función de la misma tenemos accesibles
distintos tipos de APIs de sonarqube:

  • @Batch: para la ejecución de componentes dentro del proceso de análisis de código,
  • @ComputeEngineSide: para generar informes en base al análisis de código con la posibilidad de persistir la información en la base de datos,
  • @ServerSide: para generar componentes del lado del servidor, que respondan a llamadas HTTP

Nuestro objetivo es integrarnos en la UI de sonarqube, configurar un widget que permita realizar una llamada a un componente de servidor y probar a descargar información sobre un análisis ya realizado.

5.1. UI widget.

Lo primero que vamos a hacer es configurar un widget que permita la inclusión de un componente
visual en el dashboard a nivel de proyecto. Tanto los componentes como los controladores se programan en Ruby, más bien en Ruby On Rails.

Para ello, creamos una clase que implemente RubyRailsWidget con un código como el siguiente:

package com.autentia.sonar.plugins.widgets;

import org.sonar.api.web.AbstractRubyTemplate;
import org.sonar.api.web.Description;
import org.sonar.api.web.RubyRailsWidget;
import org.sonar.api.web.WidgetProperties;
import org.sonar.api.web.WidgetProperty;
import org.sonar.api.web.WidgetPropertyType;
import org.sonar.api.web.WidgetScope;


@Description("Exports Autentia audit report")
@WidgetScope(org.sonar.api.web.WidgetScope.PROJECT)
@WidgetProperties({
  @WidgetProperty(key = "max", type = WidgetPropertyType.INTEGER, defaultValue = "80")
})
public class AuditWidget extends AbstractRubyTemplate implements RubyRailsWidget {
	public String getId() {
		return "autentia_widget";
	}

	public String getTitle() {
		return "Autentia audit widget";
	}

	protected String getTemplatePath() {
		return "/widgets/audit.erb";
	}

}

Proporcionamos un identificador de widget, un título y una ruta a la plantilla que tendrá la «lógica de presentación».

Este componente de tipo widget lo tenemos que registrar en la clase que implementa la interfaz Plugin
y que previamente ya hemos configurado en el pom.xml como pluginClass:

  package com.autentia.sonar.plugins;

  import java.util.Arrays;

  import org.sonar.api.Plugin;

  import com.autentia.sonar.plugins.widgets.AuditWidget;
  import com.autentia.sonar.ws.ExportAuditReportWS;

  public class AuditPlugin implements Plugin {

  	@Override
  	public void define(Context context) {
  		context.addExtensions(Arrays.asList(AuditWidget.class));
  	}
  }

Un último paso, sería crear la plantilla del widget, bajo la carpeta de resources de nuestro proyecto maven,
en la ubicación indicada widgets, con un código similar al siguiente:

  <span class="widget-label">Informe de auditoría&ly;/span>
  <p><a href="<%= ApplicationController.root_context -%>/api/reports/audit/export?project=<%= @snapshot.project_id %>">Descarga</a></p>
  <br />

Lo único que estamos haciendo por ahora es incluir un componente visual con un enlace que permite la invocación a un componente de servidor al que le pasaremos como parámetro el id del proyecto.

compilamos, empaquetamos, copiamos el artefacto y redesplegamos…

A nivel visual, en el dashboard del proyecto, podremos añadir el widget:

sonarqube-custom-plugin-03

Y hacer uso también del mismo

sonarqube-custom-plugin-04

Aunque ahora invoca a un recurso del servidor que aún no existe.

La renderización del widget también se puede probar invocando a la siguiente URL con el id
del widget:

http://192.168.99.100:9000/widget?id=autentia_widget

5.2. Controlador en ROR.

La primera opción que tenemos para implementar un componente del lado del servidor es crear
un controlador en ruby, que reciba la petición con el id del proyecto y devuelva información
sobre las métricas del mismo.

Para registrar un controlador debemos configurar una aplicación ROR añadiendo en la ruta
org/sonar/ror/tntaudit/ un fichero init.rb

Dentro de esa misma ruta crearemos una carpeta /app/controllers/ y otra /app/view de modo
que tendremos una estructura de directorios como la siguiente:

sonarqube-custom-plugin-05

El código del controlador podría tener el siguiente contenido como prueba de
descarga de información del proyecto

  class ExportProjectIssuesController < ApplicationController require 'builder' def index if params[:id] @project=Project.by_key(params[:id]) return project_not_found unless @project end xml_data = "" xml = Builder::XmlMarkup.new(:target => xml_data, :indent => 2 )
  	xml.instruct! :xml, :encoding => "UTF-8"

  	xml.project {
  		xml.comment! @project.attributes.inspect
  		xml.id @project.uuid
  		xml.name @project.name
  	}

    send_data( xml_data, :filename => "#{@project.name}.xml" , :type => "application/xml" )

    end

    private

    def project_not_found
      flash[:error] = message('dashboard.project_not_found')
      redirect_to :controller => 'dashboard', :action => 'index'
    end

  end

Para invocar al controlador no tenemos más que realizar una llamada a la siguiente URL,
http://192.168.99.100:9000/export_project_issues/index?id=1,
no hay prefijos, por eso recomiendan tener cuidado con la nomenclatura para no pisar los controladores
propios de la aplicación.

El resultado de la invocación será un código como el siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<project>
  <!-- {"authorization_updated_at"=>1467990070180, "copy_resource_id"=>nil, "created_at"=>Fri Jul 08 15:01:10 +0000 2016, "deprecated_kee"=>"com.autentia.tnt:tnt-labs", "description"=>nil, "enabled"=>true, "id"=>1, "kee"=>"com.autentia.tnt:tnt-labs", "language"=>nil, "long_name"=>"tnt-labs", "module_uuid"=>nil, "module_uuid_path"=>".AVXLBtOQndgnqZ1jOKLd.", "name"=>"tnt-labs", "path"=>nil, "person_id"=>nil, "project_uuid"=>"AVXLBtOQndgnqZ1jOKLd", "qualifier"=>"TRK", "root_id"=>nil, "scope"=>"PRJ", "uuid"=>"AVXLBtOQndgnqZ1jOKLd"} -->
  <id>AVXLBtOQndgnqZ1jOKLd</id>
  <name>tnt-labs</name>
</project>

No hay documentado un API que permita el acceso a las métricas o las violaciones de código de un proyecto desde un controlador Ruby,
este tipo de componente está más orientado a generar una vista, renderizaría por defecto una view en la ruta
/ror/tntaudit/app/views/export_project_issues, como el nombre del controlador y la acción definida /index.erb;
convención sobre configuración!

La idea sería generar una vista HTML y con el soporte del API javascript del propio sonarqube consumir los servicios REST
que permiten el acceso a la información de métricas de un proyecto.

Cabría también la posibilidad de invocar a un componente Java desde el controlador Ruby, con un código como el siguiente:

auditReport = Api::Utils.java_facade.getComponentByClassname('tntaudit', 'com.autentia.sonar.plugins.reports.AuditReport')
xml_data = auditReport.generate(@project.kee)

Donde AuditReport es una clase java que se encuentra en el paquete com.autentia.sonar.plugins.reports y tntaudit el identificador de nuestro plugin.

Esa clase podría tener la lógica de generación de información si bien para acceder a la misma la propuesta de sonarqube es hacer uso del API REST y no está
especialmente pensada la inyección de dependencias para disponer de ese API en un componente Java.

5.3. Web service en java.

Hay una alternativa al uso de controladores Ruby si lo que pretendemos es únicamente descargar información sobre métricas del proyecto
en un formato estándar; es el uso de servicios REST programados en Java.

Para crear un servicio web debemos añadir una clase con un código como el siguiente:

  package com.autentia.sonar.ws;

  import org.sonar.api.server.ws.Request;
  import org.sonar.api.server.ws.RequestHandler;
  import org.sonar.api.server.ws.Response;
  import org.sonar.api.server.ws.WebService;

  import com.autentia.sonar.plugins.reports.AuditReport;

  public class ExportAuditReportWS implements WebService {

  	private static final String PROJECT_KEY = "project";

  	@Override
  	   public void define(Context context) {
  	     NewController controller = context.createController("api/reports/audit");
  	     controller.setDescription("Export audit report");

  	     controller.createAction("export")
  	       .setDescription("Export audit report")
  	       .setHandler(new RequestHandler() {
  	          @Override
  	         public void handle(Request request, Response response) {
  	        	  final String project = request.mandatoryParam(PROJECT_KEY);
  	           response.newJsonWriter()
  	             .beginObject()
  	             .prop("project", request.mandatoryParam(PROJECT_KEY))
  	             .prop("issues", AuditReport.getSingleton().generate(request, project))
  	             .endObject()
  	             .close();
  	         }
  	      })
  	      .createParam(PROJECT_KEY).setDescription("Project key").setRequired(true);

  	    controller.done();
  	   }
  }

Se pueden definir parámetros obligatorios o no.

El servicio hay que registrarlo en el plugin junto con el widget:

  public class AuditPlugin implements Plugin {

  	@Override
  	public void define(Context context) {

  		context.addExtensions(Arrays.asList(AuditWidget.class, ExportAuditReportWS.class));
  	}

  }

Para consumir el servicio REST bastaría con
realizar una invocación vía GET a la siguiente URL: http://192.168.99.100:9000/api/reports/audit/export?project=com.autentia.tnt:tnt-labs,
la idea es realizar esa llamada desde el código HTML del widget.

Ahora sí, desde el servicio web o desde una clase delegada y apoyándonos en la petición original
pueden desencadenar peticiones haciendo uso de una conexión local a servicios web del api pública desde
nuestro servicio web, que es el camino recomendado por el fabricante.

  final org.sonarqube.ws.client.issue.SearchWsRequest searchIssuesRequest = new org.sonarqube.ws.client.issue.SearchWsRequest();
  searchIssuesRequest.setProjectKeys(Arrays.asList(new String[] { project }));
  final WsClient wsClient = WsClientFactories.getLocal().newClient(request.localConnector());
  final org.sonarqube.ws.Issues.SearchWsResponse issues = wsClient.issues().search(searchIssuesRequest);

6. Referencias.

    • http://docs.sonarqube.org/display/DEV/Build+Plugin
    • http://docs.sonarqube.org/display/DEV/Web+API
    • http://docs.sonarqube.org/display/DEV/Extending+Web+Application

7. Conclusiones.

Una vez estudiadas las posibilidades y teniendo acceso a las métricas y violaciones del proyecto,
solo nos queda programar la exportación en el formato que elijamos.

A disfrutarlo!.

Un saludo.

Jose

2 COMENTARIOS

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad