Aller au contenu

Dagger SonarQube

Nous commençons à prendre en main Dagger avec nos différents tests précédents, mais il ne faut pas oublier de vérifier notre qualité logicielle ! 😎

SonarQube

SonarQube est un outil de qualité logicielle open source qui permet de vérifier la qualité de notre code en s’appuyant des bonnes pratiques, clean code. SonarQube

Il y a bien sûr d’autres fonctionnalités, mais ce n’est pas le sujet, ici on va voir comment faire l’analyse sonar dans notre CI/CD avec Dagger.

Il existe un module pour Dagger, mais je n’ai pas réussi à le faire fonctionner avec l’URL d’un sonar self hosted.

Daggerverse Sonar

J’essayerai de faire une Pull Request ou une Issue au mainteneur du module pour résoudre ce problème (Ou peut être une skill issue 🫣)

On va donc le faire nous même ! Au final c’est juste un container SonarCLI donc ça ne devrait pas être difficile.

Intégration

On va donc reprendre notre projet lib-acl-json.

Nous avons déjà les fonctions suivantes qui permettent d’installer les packages et de réaliser les tests unitaires.

@func()
async test(source: Directory): Promise<Directory> {
return this.buildEnv(source).withExec(["bun", "test"]).directory("/src");
}
@func()
buildEnv(source: Directory): Container {
const nodeCache = dag.cacheVolume("node");
return dag
.container()
.from("oven/bun")
.withDirectory("/src", source)
.withMountedCache("/root/.npm", nodeCache)
.withWorkdir("/src")
.withExec(["bun", "install"]);
}

Sonar scanner CLI

Comme Dagger nous permet de lancer de travailler avec des conteneurs nativement, nous allons en profiter et utiliser l’image de Sonnar Scanner

Voici la fonction de scan :

@func()
async sonar(source: Directory, url: string, token: Secret): Promise<string> {
return dag
.container()
.from("sonarsource/sonar-scanner-cli")
.withUser("root")
.withDirectory("/usr/src", source)
.withSecretVariable("SONAR_TOKEN", token)
.withEnvVariable("SONAR_HOST_URL", url)
.withExec(["sonar-scanner"])
.stdout();
}

Nous allons faire en async comme les tests pour bien attendre la fin de l’analyse avant de passer à la suite.

.stdout() permet de retourner le résultat en string, donc notre promise est bien un string, si vous souhaitez, vous pouvez le retirer pour retourner une Promise container

Nous avons besoin aussi de lancer l’analyse en root avec .withUser("root"), sinon le container n’aura pas les droits pour l’analyse.

Voici quelques informations à propos de la fonction, on test ??

On va d’abord mettre notre token en ENV :

Fenêtre de terminal
export SONAR_TOKEN=sqa_8172debfdbzdjhugYA66871Y8gdybndeazudaojidpaldoqdlq

Et maintenant on lance notre fonction avec dagger call :

dagger call sonar --source=. --url=https://your-url-sonar.fr --token=env:SONAR_TOKEN

Dagger call sonar Dagger Result sonar

Notre analyse c’est réalisé avec succès ! 🎉

Mais, la nous avons pas le code coverage dans sonar car nous n’avons pas réalisé la fonction de tests et récupérer les fichiers générés par bun test.

On va donc créer une fonction pour exécuter les différentes fonctions.

@func()
async minimal(
source: Directory,
url: string,
token: Secret
): Promise<Container> {
// Install dependencies and get container in variable for return
const preBuild = this.buildEnv(source);
// Run tests and push in variable for get directory with code coverage
const unitTest = await this.test(preBuild.directory("/src"));
// Run sonar
await this.sonar(unitTest, url, token);
// Return the container bun install
return preBuild;
}

On return preBuild, pour avoir le répertoire avec le bun install afin de build le projet plus tard dans une autre fonction.

On va stocker le Directory après les tests dans notre variable unitTest, comme ça, nous aurons les résultats de coverage pour l’analyse sonar.

Lançons notre fonction minimal

Fenêtre de terminal
dagger call minimal --source=. --url=https://your-url-sonar.fr --token=env:SONAR_TOKEN

Résultat :

Dagger Result sonar minimal

Nous voyons que nous avons bien le bun install, ensuite le test et enfin notre analyse sonar.

Nous avons bien notre % de coverage du projet sur notre interface sonar.

Dagger Result sonar minimal

Pourquoi faire un fonction minimal ?

La fonction minimal va permettre aux développeurs de lancer l’analyse sonar sans forcement build tout le projet. Puis s’ils veulent build le projet juste après, le cache de Dagger va rendre le processus plus rapide.

Voici mon fichier index.ts au complet :

import {
dag,
Container,
Directory,
object,
func,
Secret,
} from "@dagger.io/dagger";
@object()
export class LibAclJson {
/*
Prod part :
Publish : install dependencies, run tests, sonar, build, publish
Build : install dependencies, run tests, sonar, build,
*/
@func()
async publish(
source: Directory,
version: string,
token: Secret,
url_sonar: string,
token_sonar: Secret
): Promise<void> {
// Build the source code
const buildOutput = await this.build(source, url_sonar, token_sonar);
const publishContainer = dag
.container()
.from("node:lts")
.withWorkdir("/src")
.withDirectory("/src", buildOutput)
//Create environment variable in container
.withSecretVariable("TOKEN", token)
// Create .npmrc file
.withExec([
"sh",
"-c",
'echo "//registry.npmjs.org/:_authToken=${TOKEN}" > /root/.npmrc',
])
// Choose the version to publish
.withExec(["npm", "version", version, "--no-git-tag-version"])
// Publish the package
.withExec(["npm", "publish", "--access public"]);
await publishContainer.exitCode();
}
@func()
async build(
source: Directory,
url: string,
token: Secret
): Promise<Directory> {
const preBuild = await this.minimal(source, url, token);
const buildContainer = preBuild.withExec([
"bun",
"build",
"src/index.ts",
"--outdir",
"./dist",
"--target",
"node",
]);
const outputDir = buildContainer.directory("/src");
return outputDir;
}
/*
Dev part :
Minimal : install dependencies, run tests, sonar
Sonar : run sonar
Test : run bun tests
Build : run bun build
*/
@func()
async minimal(
source: Directory,
url: string,
token: Secret
): Promise<Container> {
// Install dependencies and get container in variable for return
const preBuild = this.buildEnv(source);
// Run tests and push in variable for get directory with code coverage
const unitTest = await this.test(preBuild.directory("/src"));
// Run sonar
await this.sonar(unitTest, url, token);
// Return the container bun install
return preBuild;
}
@func()
async sonar(source: Directory, url: string, token: Secret): Promise<string> {
return dag
.container()
.from("sonarsource/sonar-scanner-cli")
.withUser("root")
.withDirectory("/usr/src", source)
.withSecretVariable("SONAR_TOKEN", token)
.withEnvVariable("SONAR_HOST_URL", url)
.withExec(["sonar-scanner"])
.stdout();
}
@func()
async test(source: Directory): Promise<Directory> {
return this.buildEnv(source).withExec(["bun", "test"]).directory("/src");
}
@func()
buildEnv(source: Directory): Container {
const nodeCache = dag.cacheVolume("node");
return dag
.container()
.from("oven/bun")
.withDirectory("/src", source)
.withMountedCache("/root/.npm", nodeCache)
.withWorkdir("/src")
.withExec(["bun", "install"]);
}
}

Je vais juste lancer un build pour vous montrer la mécanique de cache.

dagger call build --source=. --url=https://your-url-sonar.fr --token=env:SONAR_TOKEN

4,4s le build !

Dagger Cloud web result

Comme nous utilisons le cache, il y a juste l’étape du build à réaliser finalement.

Dagger Cloud web result

Conclusion

Comme vous l’avez vu, l’intégration est vraiment simple, étant donné que la plupart des outils est maintenant conteneurisé, cela nous facilite le travail et optimise le processus de CI/CD. C’est aussi ce qui rend Dagger très efficace, il permet de réaliser des pipelines en local qui sera très proche de l’environnement de production grâce aux conteneurs.

Github

Lien du projet : lib-acl-json