Création du projet

Le projet est basé sur le framework Micronaut.

Comme pour Spring ou d’autres frameworks, depuis leur site, il est possible de “préparer”" le projet pour ensuite en récupérer le squelette.

Il faut donc se rendre sur [Micronaut Launch](Micronaut Launch)

De là, on peut choisir le type d’application et les différentes versions des éléments à utiliser.

Je commence donc avec un application de type “Command Line Application”, en Java 11 (pour une compatibilité un peu plus large).

Mon outil de build par défaut est Maven et mon framework de test JUnit.

Ecran d'initialisation de Micronaut Launch

Une fois fait, on peut cliquer sur le bouton “Generate Project” et récupérer un zip qui contient les éléments de base.

Une fois fait, il faut alors ajouter la dépendance qui va permettre d’interagir avec Docker dans le pom

    <dependency>
      <groupId>com.github.docker-java</groupId>
      <artifactId>docker-java</artifactId>
      <version>3.2.12</version>
    </dependency>

Personnalisation du code

J’ai décidé de répartir le code entre deux packages :

  • cli qui va contenir tous les éléments dédiés à la CLI

  • docker qui va contenir toutes les commandes Docker

Quelle originalité !

Une petite méthode privée pour récupérer le client Docker

    private DockerClient getClient() {
        return DockerClientBuilder.getInstance().build();
    }

Une autre méthode, publique, qui va permettre de récupérer la liste des conteneurs présents sur la machine.

    public List<Container> listContainers() {
        return getClient().listContainersCmd()
                .withShowSize(true)
                .withShowAll(true)
                .exec();
    }

Côté CLI, la classe utilise plusieurs annotations spécifiques à Picocli :

@Command(name="containers", description = "Containers commands", mixinStandardHelpOptions = true)
final public class ContainersCommand implements Runnable {

    @Option(names = {"-l", "--list"}, description = "List all containers on the machine")
    boolean list;

    @Override
    public void run() {

        ContainerControl control = new ContainerControl();

        if (list) {
            List<Container> containers = control.listContainers();

            if (CollectionUtils.isEmpty(containers)) {
                System.out.println("No containers");
                return;
            }

            System.out.format("%-20s%-20s\n", "Name", "Id");

            for (Container container : containers) {
                String name = container.getNames().length > 0 ? container.getNames()[0]:"No name";
                System.out.format("%-20s%-20s\n", name, container.getId());
            }

            System.out.println(control.listContainers());
        } else {
            System.out.println("Containers command");
        }
    }
}

@Command permet de définir la commande attendue pour faire appel à cette classe ainsi que la description associée.

Il est ensuite possible de définir les différentes options que la commande va supporter avec l’annotation @Option

la valeur names est un tableau permettant de spécifier l’option et son raccourci

Si la variable de classe est un booléen, sa présence passe la valeur à true, sinon elle vaut false.

Si c’est une chaîne de caractères, le framework va rechercher la valeur saisie juste après le nom de l’option.

Enfin, j’ai essayé de faire une présentation sous la forme d’un tableau d’où le formatage de l’affichage en sortie

System.out.format("%-20s%-20s\n", "Name", "Id");

Les tests

    @Test
    public void testWithContainersCommandList() throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        System.setOut(new PrintStream(baos));

        try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
            String[] args = new String[] { "containers", "-l" };
            PicocliRunner.run(DockerbisCommand.class, ctx, args);

            // dockerbis
            assertTrue(baos.toString().contains("Name"));
        }
    }

Pour les tests, il faut récupérer la sortie vers un stream et en vérifier le contenu.

Il suffit alors de faire appel au PicocliRunner en lui passant les options désirées sous la forme d’un tableau de String.

Bien évidemment, sur un environnement de CLI, la list des conteneurs risque de varier, il faudra donc passer par un Mock dans la suite des opérations.

La classe principale

Quand le projet sera compilé et exécutable, le point d’entré sera la classe principale.

Il faut donc penser à définir nos classes de commande comment des “sous-commandes” pour qu’elles soient accessibles en ajoutant des “subcommands” à l’annotation @Command.

@Command(name = "dockerbis", description = "...",
        mixinStandardHelpOptions = true, subcommands = {ContainersCommand.class})
public class DockerbisCommand implements Runnable {

    @Option(names = {"-v", "--verbose"}, description = "...")
    boolean verbose;

    public static void main(String[] args) throws Exception {
        PicocliRunner.run(DockerbisCommand.class, args);
    }

    public void run() {
        // business logic here
        if (verbose) {
            System.out.println("Hi!");
        }
    }
}