Mettre à jour les images OCI utilisées par JIB avec Renovate et un registry OCI GitLab privé
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 lepackage-lock.json
associé - Mettre à jour les dépendances maven dans un
pom.xml
ou unbuild.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).
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
.
Renovate + JIB = 💔
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.
La configuration de base
build.gradle
(en groovy dans mon cas) :jib {
from {
image = "eclipse-temurin:21.0.3_9-jre-alpine"
}
}
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 :
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 patchesrebaseWhen: 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 :
Détection de la configuration JIB
Pour l’aider, on va lui définir un customManager
de type regex
dans son fichier config.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 (iciregex
)fileMatch
: Une regex permettant de sélectionner les fichiers sur lesquels appliquer ce managermatchStrings
: 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 instructionFROM
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épendancedatasourceTemplate
: 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 statiquedocker
, sans faire de templating
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… 🥁
On voit que Renovate arrive bien à détecter l’image, et propose même de la mettre à jour dans sa dernière version !
On voit qu’il y en a qui suivent ! On y vient, chaque chose en son temps !
Avec un registry GitLab privé
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
:
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é :
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.
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
:
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, uniquementdocker
)matchPackageNames
: La liste des noms des packages concernés par cette règleversionCompatibility
: La regex qui permet d’extraire les champsversion
(qui désigne la version réelle) etcompatibility
(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
.
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é
Ma configuration Renovate complète ressemble donc à ça :
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 ! 😉