De plus en plus d’applications fournissent, en plus de leurs API (REST, gPRC, etc.), des utilitaires (Command Line Interface). Ceux-ci permettent un accès rapide via un terminal aux services proposés, sans avoir à se soucier techniquement de la communication.
Dans un contexte Cloud Native, les CLI peuvent servir à fournir une interface supplémentaire pour l’utilisation de vos services ou encore des utilitaires permettant de faciliter l’administration, le monitoring ou le débogage.
Une ligne de commande se compose de plusieurs éléments :
docker container run --name containerName --rm imageName echo Hello WeScale
La CLI fournie par Docker permet par exemple un accès simplifié à l’API exposée par le serveur local : `docker container run <....>` enchaînera la création (point d’entrée `/containers/create`) et le démarrage (point d’entrée `/container/{id}/start`) d’un conteneur.
La mise à disposition de ces utilitaires permet :
Une CLI peut être codée dans n’importe quel langage de programmation, le choix est motivé par :
Pour distribuer ces programmes, plusieurs méthodes sont possibles et dépendent du langage de programmation utilisé :
L’idée semble folle et un mur se dessine à l’horizon en pensant à l’écosystème Java et sa complexité :
Depuis quelques années des solutions de scripting Java émergent, qu’elles soient intégrées à Java (JShell), amenées par la communauté (JBang) ou rendues possibles grâce à de nouveaux JDK (GraalVM).
Environnement d'exécution | Avantages | Inconvénients |
JShell |
|
|
JBang |
|
|
GraalVm |
|
|
Pour résumer, il n’y a pas de bonnes ou de mauvaises solutions, on notera une grande facilité de distribution via l’utilisation de JBang et des performances optimisées via l’utilisation de GraalVM Native Image au détriment du temps de compilation.
En effet la question est légitime, pourquoi utiliser Java pour des scripts ayant une durée de vie courte alors qu’il excelle dans la gestion d’applications ayant une durée de vie longue ?
Tout simplement parce que maintenant c’est possible, viable et performant ! Avec l’arrivée de la compilation native, un programme Java peut être extrêmement performant dès son démarrage et permet de bénéficier de son écosystème riche :
Bien entendu, des développeurs back ayant principalement des compétences en Java seront immédiatement efficaces pour développer des CLI.
Sans entrer dans les détails, la compilation native Java est amenée par le projet native-image porté par GraalVM. Il permet de générer un binaire contenant les classes Java de l’application et des dépendances ainsi que le code natif (du moins une partie) du runtime Java.
Le binaire généré ne tourne pas dans une JVM préinstallée mais est composé de parties de la JVM ce qui le rend extrêmement portable et rapide mais sensible au système d’exploitation cible.
Évidemment, la compilation native ne présente pas que des avantages :
Quel que soit l’environnement d’exécution, Java offre une multitude de frameworks facilitant le développement d’applications. Tous ne sont pas compatibles avec la compilation native de GraalVM mais une rapide recherche permet d’obtenir une liste plutôt prometteuse (non exhaustive) :
Peu importe ses préférences, il est possible de trouver son bonheur dans les frameworks proposés. La suite de cet article se base sur l’utilisation de Quarkus pour sa simplicité d’utilisation et son environnement de développement.
Quarkus est un framework de développement d'applications Cloud Native Java basé sur Jakarta EE et Microprofile. Reposant sur un système d’extensions, l’application générée ne contient que les spécifications utiles pour votre application. Par exemple, la spécification JAX-RS ne sera pas packagée si votre application fournit un service gRPC.
Il offre un cadre de développement concentré sur la satisfaction des développeurs avec des fonctionnalités avancées comme le Live Coding, les Test Containers en encore la DevUI permettant de visualiser les composants de l’application.
Un mode ‘Command Line Application’ permet de concevoir une application de type script définissant une méthode en point d’entrée. Bien entendu, toutes les fonctionnalités avancées de Quarkus sont accessibles dès lors que l’extension est configurée.
@QuarkusMain
public class HelloWorldMain implements QuarkusApplication {
@Override
public int run(String... args) {
if (args.length == 0) {
return 1;
}
System.out.println("Hello " + args[0]);
return 0;
}
}
Voilà, vous avez écrit votre première application en ligne de commande ! Dans les coulisses, Quarkus détecte l’annotation @QuarkusMain et cherche :
Quarkus utilise un système de test basé sur Junit et sur l’annotation @QuarkusTest. Il permet d’injecter dans la classe de test toutes sortes d’objets CDI supportés par Quarkus (Context and Dependency Injection) comme des beans, des alternatives ou des mocks.
Dans le cas du mode ligne de commande, Quarkus met à disposition deux nouvelles annotations : @QuarkusMainTest et @QuarkusMainIntegrationTest. La première est utilisée sur la phase de test unitaire et dans une JVM alors que la deuxième est utilisée sur la phase de test d'intégration et contre le package produit (binaire ou JAR). Contrairement aux annotations test classiques de Quarkus, @QuarkusMainTest et @QuarkusMainIntegrationTest ne permettent pas l’injection CDI.
@QuarkusMainTest
class HelloWorldMainTest {
@Test
@Launch("World")
public void testLaunchCommand(LaunchResult result) {
Assertions.assertEquals("Hello World", result.getOutput());
}
@Test
@Launch(value = {}, exitCode = 1)
public void testLaunchCommandFailed() {}
@Test
public void testManualLaunch(QuarkusMainLauncher launcher) {
LaunchResult result = launcher.launch("Everyone");
Assertions.assertEquals(0, result.exitCode());
Assertions.assertEquals("Hello Everyone", result.getOutput());
}
}
La classe de test ci-dessus est détectée via l’annotation @QuarkusMainTest et exécute trois tests :
Pour les tests d’intégration, la mécanique est la même. L’exemple de base définit une classe annotée @QuarkusMainIntegrationTest et héritant de la classe de test précédente :
@QuarkusMainIntegrationTest
class HelloWorldMainIT extends HelloWorldMainTest { }
Ce mécanisme d’héritage est un confort d’utilisation permettant l’exécution des tests joués lors de la phase de test unitaire (en mode JVM) sur le livrable final produit par Quarkus (JAR exécutable ou binaire).
C’est une facilité proposée par la documentation officielle de Quarkus qui n'est en aucun cas obligatoire. Libre à vous d’implémenter des tests d’intégration spécifiques en fonction de vos usages en utilisant les annotations @Launch et @Test.
Maintenant que vous êtes un(e) expert(e) du mode ligne de commande Quarkus, vous êtes heureux(se), mais vous ne voyez pas comment son utilisation sera plus pratique pour développer une CLI par rapport à Java SE. C’est là qu’intervient l’intégration de Picocli dans l’écosystème de Quarkus.
Picocli est une librairie Java gérant pour vous l’analyse des arguments de votre ligne de commande. Son API est riche et permet rapidement de gérer :
@Command(name = "greeting", mixinStandardHelpOptions = true)
public class GreetingCommand implements Runnable {
@Option(names = {"-s", "--secret"},
description = "Secret greeting message",
interactive = true)
String secret;
@Parameters(defaultValue = "picocli", description = "Your name.")
String name;
@Override
public void run() {
System.out.println("Hello " + name + "!");
ofNullable(secret).ifPresent(s -> System.out.println(
"You have a secret greeting message: " + s
));
}
}
L’activation de l’extension amène quelques changements sur notre code :
Une multitude de fonctionnalités sont disponibles pour la gestion des paramètres et options. Ici, l’option secret est interactive : Picocli se chargera de proposer à l’utilisateur de saisir sa valeur (sans l’afficher sur le terminal) avant de lancer l’exécution de la commande. Ci-dessous, vous trouverez la compilation et l’exécution de ce code en version JVM puis en version native.
Compilation et exécution d’un JAR :
Compilation native et exécution :
Sans réaliser de véritables benchmarks nous pouvons noter deux points importants :
L’utilisation d’une CLI dans un environnement cloud peut être motivée par de multiples raisons. Ici, deux illustrations vous sont proposées en partant d’une application “Greeting” tournant dans un cluster Kubernetes.
Souvent, les images déployées sont minimales et ne disposent que des paquets systèmes nécessaires (donc pas d’accès à curl ou bash) et l’équipe de développement n’a pas eu le temps de développer une interface d’administration web (toute ressemblance à une situation réelle est fortuite).
Une solution relativement simple pour effectuer nos tâches d’administration consiste à utiliser la CLI d’administration dans un Job Kubernetes. Ils permettent d’exécuter des tâches en soumettant un manifeste Kubernetes et possèdent plusieurs avantages :
Avec une image Docker contenant notre CLI, un job Kubernetes se définit comme le code suivant :
apiVersion: batch/v1
kind: Job
metadata:
name: blog-quarkus-cli-greetings-cli
spec:
template:
spec:
containers:
- name: blog-quarkus-cli-greetings-cli
image:
ktoublanc/blog-quarkus-cli-greetings-cli:1.0.0-SNAPSHOT-native
command: [ "./application", "admin", "set", "Hey" ]
restartPolicy: Never
Son exécution crée un Pod (objet portant les conteneurs dans Kubernetes) qui utilise l’image Docker spécifiée et lance la commande d’administration, changeant ainsi le message de salutation par “Hey”.
Prenons maintenant un second exemple, notre conteneur tourne et nous voulons faire une administration mais sans créer un job Kubernetes. Depuis la version 1.25 de l’API Kubernetes, les conteneurs éphémères sont stables. Ils sont conçus pour permettre le débogage de Pod lorsque “kubectl exec” est insuffisant et permettent d’obtenir toutes les informations du processus lancé dans le conteneur “nominal”.
Dans notre cas, l’utilisation d’un conteneur éphémère permet :
Kubernetes expose une API dédiée pour la gestion des conteneurs éphémères, elle est accessible via la command “kubectl debug” :
kubectl debug -it \
--image <BASE IMAGE AND TAG> \
--target <CONTAINER NAME FROM THE POD DESCRIPTION> \
<YOUR POD IDENTIFIER>
Plutôt que d’exposer les commandes nécessaires, l'enregistrement du terminal suivant vous guidera dans les étapes.
Vous trouverez toutes les ressources (code source, exemple de CI, manifestes Kubernetes et images Docker) de cet article disponible sur GitLab :
https://gitlab.com/ktoublanc-ws/quarkus-cli-blog
Grâce à l'ensemble des évolutions de la JVM, il est maintenant possible de réaliser des CLI performantes en Java en bénéficiant de son écosystème riche, permettant ainsi de gagner en rapidité et qualité sans devoir apprendre un nouveau langage sur le tas !
Si une compilation native est choisie, les temps de compilation seront drastiquement augmentés mais les temps d’exécution seront considérablement diminués et la distribution dans un conteneur Docker sera simplifiée.