Mettre à jour les images OCI utilisées par JIB avec Renovate et un registry OCI GitLab privé

Question
Dis Papa, c’est quoi Renovate ?

Renovate est un outil très puissant qui vous permet de mettre à jour automatiquement les dépendances de votre projet via l’ouverture de PR/MR. Il supporte de nombreuses technologies, dont Docker, Helm, Maven, Gradle, npm, etc. et fonctionne de manière relativement automagique pour les cas standards :

  • Mettre à jour les dépendances npm dans un package.json et le package-lock.jsonassocié
  • Mettre à jour les dépendances maven dans un pom.xml ou un build.gradle
  • Mettre à jour l’image de base utilisée par un Dockerfile

Si vous ne connaissez pas encore Renovate, je vous recommande d’aller le découvrir sur le blog de l’ami Stéphane Robert, qui explique très bien son fonctionnement (comme d’habitude), et de le mettre en place au plus vite sur vos projets (parce que la mise à jour manuelle des dépendances c’est fastidieux, donc on le fait jamais rarement).

Question
Ah d’accord, ça a l’air génial ! Et… JIB, c’est quoi ?

JIB, c’est l’acronyme de Java Image Builder, un outil Java développé par Google qui permet de construire des images de container OCI directement dans une JVM, sans avoir besoin de Docker, Podman, ou autre container runtime.

Il est notamment très facile à utiliser à travers les plugins Maven JIB et Gradle JIB, qui lui premettent de s’intégrer au cycle de vie de l’application via des commandes mvn ou gradle.

Avertissement
Pour la suite de ce billet, je vais considérer que vous utilisez déjà JIB et Renovate sur votre projet. Je vous laisse vous référer aux quickstarts Gradle, JIB et Renovate pour ça.

Mais comme le mélange de 2 bonnes choses ne donne pas toujours un bon résultat (vous avez essayé de mélanger chocolat et camembert ?), Renovate et JIB, nativement, c’est pas l’amour fou.

Comme je l’ai dit en intro, Renovate fonctionne de manière relativement automagique pour les cas standards. Or, le plugin JIB de Maven et Gradle n’est pas un cas standard de dépendance Maven/Gradle. Renovate est donc incapable de détecter tout seul l’image OCI utilisée comme base pour les builds d’image OCI.

Heureusement, Renovate permet de déclarer des configurations custom afin de gérer les cas spécifiques comme celui-là. On utilise pour ça les concepts de packageRule et de customManager. Mais commençons déjà par le commencement.

Remarque
Nous utiliserons ici Gradle en exemple, avec cette configuration JIB dans le fichier build.gradle (en groovy dans mon cas) :

groovy

jib {
    from {
        image = "eclipse-temurin:21.0.3_9-jre-alpine"
    }
}
Remarque
J’utilise volontairement la version 21.0.3_9-jre-alpine, qui n’est pas la dernière version 21.x disponible à ce jour, vu qu’on veut que Renovate nous propose une montée de version en 21.0.4_7-jre-alpine.

Quant à ma configuration Renovate, elle est relativement minimaliste :

js

module.exports = {
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
        "config:recommended",
        ":prHourlyLimitNone"
  ],
  "major": {
      "enabled": false
  },
  "rebaseWhen": "behind-base-branch",
  "labels": ["Renovate"],
  "assignees": ["lgatellier"],
}

Comme vous le voyez, j’utilise seulement quelques paramètres assez généralistes :

  • major.enabled: false : Je désactive les montées de version majeures. Je préfère les faire manuellement, et laisser Renovate gérer les mises à jour mineures et les patches
  • rebaseWhen: behind-base-branch : J’indique à Renovate de rebase automatiquement ses branches lorsqu’elles ne sont plus à jour

Dans cette situation, Renovate ne détecte même pas que mon fichier build.gradle contient une référence à une image de container :

Capture de l’issue Dependency Dashboard créée par Renovate, ne montrant pas l’image utilisée par JIB

Pour l’aider, on va lui définir un customManager de type regex dans son fichier config.js :

js

module.exports = {
   "$schema": "https://docs.renovatebot.com/renovate-schema.json",
   "extends": [
      "config:recommended",
      ":prHourlyLimitNone"
   ],
   "major": {
      "enabled": false
   },
   "rebaseWhen": "behind-base-branch",
   "labels": ["Renovate"],
   "assignees": ["lgatellier"],
   "customManagers": [{
     "customType": "regex",
     "fileMatch": ["^.+\.gradle$"],
     "matchStrings": ["image\\s*=\\s*['\"](?<depName>.*):(?<currentValue>(.*)?)@?(?<currentDigest>(sha256:[a-f0-9]+)?)['\"]"],
     "autoReplaceStringTemplate": "image = \"{{{depName}}}:{{{newValue}}}{{#if newDigest}}@{{{newDigest}}}{{/if}}\"",
     "datasourceTemplate": "docker",
   }]
}

Dans le jargon Renovate, un manager, c’est un composant en charge de la détection et de la mise à jour de certaines dépendances. Nativement, il existe un manager correspondant à chaque package manager supporté : npm, gradle, maven, poetry, cargo, composer, etc.

Ici, on déclare un custom manager avec les indications suivantes :

  • customType : Le type du Custom Manager (ici regex)
  • fileMatch : Une regex permettant de sélectionner les fichiers sur lesquels appliquer ce manager
  • matchStrings : Une liste de regex permettant de détecter les chaînes de caractères qui correspondent à ce qui est géré par ce manager (dans notre cas, une image de container utilisée comme instruction FROM par JIB)
  • autoReplaceStringTemplate : Un template (attention, ce n’est pas de la regex, mais du templating ici) permettant d’effectuer le remplacement de la chaîne de caractères détectée afin d’appliquer la mise à jour de la dépendance
  • datasourceTemplate : Un template (encore un) permettant de choisir la datasource, c’est-à-dire la source des montées de version. Dans notre cas, il s’agira systématiquement d’images de containers, donc on met juste la valeur statique docker, sans faire de templating
Astuces
Les mots-clés depName, currentValue, currentDigest, newValue, depName sont des champs reconnus nativement par le moteur de templating de Renovate. La liste exhaustive des champs existants est disponible sur [la documentation Renovate][6].

Une fois cette configuration en place, je relance l’exécution de Renovate et… 🥁

Capture de l’issue Dependency Dashboard créée par Renovate, montrant bien l’image utilisée par JIB et proposant sa mise à jour

On voit que Renovate arrive bien à détecter l’image, et propose même de la mettre à jour dans sa dernière version !

Question
OK c’est bien, mais t’avais pas parlé de registry privé ?

On voit qu’il y en a qui suivent ! On y vient, chaque chose en son temps !

Dans mon cas, l’image de base utilisée par JIB vient d’un registry OCI privé (celui de mon instance GitLab), car je fournis des golden images aux équipes de dev avec de la configuration qui va bien : certificats corporate auto-signés, user non-root, etc.

Et là… ça se corse un peu.

Voyons mon build.gradle :

groovy

jib {
    from {
        image = "registry.gitlab.com/lgatellier/renovate-jib/eclipse-temurin:21.0.3_9-jre-alpine"
    }
}

Commençons par apprendre à Renovate à se connecter à mon registry privé :

js

module.exports = {
   // ...
   "hostRules": [{
      "hostType": "docker",
      "matchHost": "registry.gitlab.com",
      "username": "renovate-ci-pipeline",
      "password": process.env.RENOVATE_TOKEN
   }]
}

Ici, j’indique à Renovate que la connexion à mon registry privé s’effectue en utilisant la variable d’environnement RENOVATE_TOKEN.

Mais je vais être confronté ici à un autre problème. Dans mon registry, j’ai plusieurs déclinaisons/suffixes de cette image :

  • -jre : Mon image du JRE en version “stable”
  • -jre-debug : La version “stable” en mode “debug”, avec des outils supplémentaires ajoutés dans l’image
  • -jre-main : Provenant du build automatique depuis ma branche main
  • -jre-debug-main : La version en mode “debug” provenant du build automatique depuis ma branche main
  • Etc.

Et dans ce cas, Renovate se mélange un peu les pinceaux dans les suffixes, qui ne font pas à proprement parler de la version, mais indiquent juste quelle est la déclinaison/distribution de l’image.

Bug
Pour une raison que je n’explique pas encore, Renovate ne semble pas se mélanger les pinceaux avec le Docker Hub, mais uniquement dans le cas d’un registry OCI GitLab.

Pour gérer ça correctement, Renovate propose la fonctionnalité de package rules, qui permet de définir des règles fines pour un package donné. J’ajoute donc une règle dans mon fichier config.js :

js

module.exports = {
   // ...
   "customManagers": [{
     "customType": "regex",
     "fileMatch": ["^.+\.gradle$"],
     "matchStrings": ["image\\s*=\\s*['\"](?<depName>.*):(?<currentValue>(.*)?)@?(?<currentDigest>(sha256:[a-f0-9]+)?)['\"]"],
     "autoReplaceStringTemplate": "image = \"{{{depName}}}:{{{newValue}}}{{#if newDigest}}@{{{newDigest}}}{{/if}}\"",
     "datasourceTemplate": "docker",
     "versioningTemplate": "maven"
   }],
    "packageRules": [{
        "matchDatasources": ["docker"],
        "matchPackageNames": ["registry.gitlab.com/lgatellier/renovate-jib/eclipse-temurin"],
        "versionCompatibility": "^(?<version>[^-]+)(?<compatibility>-.*)?$",
    }]   
}

Dans la section packageRules, je déclare une règle qui décrit :

  • matchDatasources : La liste des datasources auxquelles la règle s’applique (ici, uniquement docker)
  • matchPackageNames : La liste des noms des packages concernés par cette règle
  • versionCompatibility : La regex qui permet d’extraire les champs version (qui désigne la version réelle) et compatibility (qui désigne la compatibilité/famille/distribution/etc.)

J’ajoute également un paramètre dans mon custom manager pour spécifier que le versioning est au format maven, car les versions de l’image eclipse-temurin sont au format MAJOR.MINOR.PATCH_BUILD, ce qui n’est pas supporté par le détecteur de versioning par défaut qui est semver-coerced.

Astuces
Pour plus d’informations sur les détecteurs de versioning, voir la documentation de Renovate à ce sujet.

Je relance mon pipeline Renovate, et cette fois, c’est bon, tout fonctionne comme attendu :

  • Renovate détecte bien la présence d’une référence à une image OCI
  • Renovate détecte correctement le numéro de version dans le tag de l’image et propose les mises à jour en conservant le suffixe inchangé

Capture d’une MR de Renovate gérant correctement les images de mon registry privé

Ma configuration Renovate complète ressemble donc à ça :

js

module.exports = {
   "$schema": "https://docs.renovatebot.com/renovate-schema.json",
   "extends": [
      "config:recommended",
      ":prHourlyLimitNone"
   ],
   "major": {
      "enabled": false
   },
   "rebaseWhen": "behind-base-branch",
   "labels": ["Renovate"],
   "assignees": ["lgatellier"],
   "hostRules": [{
      "hostType": "docker",
      "matchHost": "registry.gitlab.com",
      "username": "renovate-ci-pipeline",
      "password": process.env.RENOVATE_TOKEN
   }],
   "customManagers": [{
      "customType": "regex",
      "fileMatch": ["^.+\.gradle$"],
      "matchStrings": ["image\\s*=\\s*['\"](?<depName>.*):(?<currentValue>(.*)?)@?(?<currentDigest>(sha256:[a-f0-9]+)?)['\"]"],
      "autoReplaceStringTemplate": "image = \"{{{depName}}}:{{{newValue}}}{{#if newDigest}}@{{{newDigest}}}{{/if}}\"",
      "datasourceTemplate": "docker",
      "versioningTemplate": "maven"
   }],
   "packageRules": [{
      "matchDatasources": ["docker"],
      "matchPackageNames": ["registry.gitlab.com/lgatellier/renovate-jib/eclipse-temurin"],
      "versionCompatibility": "^(?<version>[^-]+)(?<compatibility>-.*)?$",
   }],
}

J’espère que ce billet vous évitera les mêmes prises de tête que moi, et vous économisera les 2 jours de boulot que j’ai passés sur le sujet !

N’hésitez pas à me contacter via Twitter/X, LinkedIn ou autre pour échanger à ce sujet ! 😉

Remarque
Au moment où je rédige ce billet, j’utilise un serveur GitLab 17.3 et Renovate 37.377.4.