Partie 3: Gestion des workspaces
Bonjour à toutes et à tous 😀
Dans le précédent article, nous avons créé un pipeline de création de package à la fois de release et de pre-release pour un projet mono-crate.
Mais en Rust, il existe un mécanisme très utilisés qui se nomme les workspaces.
Ce sont des environnement de crates liées qui permettent de découper un projet lorsqu'il devient trop volumnineux ou lorsque le besoin de partager des données se fait sentir.
Workspace
Pour créer un workspace, on peut taper.
Une ligne va venir se rajouter dans le Cargo.toml
[]
= ["w1"]
Et on peut rajouter autant de sous crates que l'on veut
Si l'on tente de faire
Il va le faire pour w1
, w2
et w3
.
C'est en soit cool, mais pas très intéressant pour nous.
Généralement les crates d'un workspace ont des cycles de versions disjoint, autrement dit ce n'est pas parce que w1
change de version que c'est le cas de w2
et de w2
.
Heureusement il est possible de scoper le package à créer
Parfait et maintenant les métadonnées.
Il est nécessaire au bon fonctionnement de la registry et ce n'est pas grave en soit que le workspace complet y soit décrit.
On vient juste s'assurer que les dépendences ne soient pas prise en compte avec
On va donc utiliser une variable CRATE_PACKAGE
variables:
CRATE_PACKAGE: ""
Qui va venir contrôler le package à créer.
Ok, et donc on contrôle comment cette variable ?
Alors on peut le faire depuis les variables de CI externes, mais ce n'est pas pratique.
Au lieu de ça, je vous propose d'utiliser ce que l'on appelle les matrix, cela permet de faire tourner en parrallèle plusieurs jobs d'un même type mais configuré différemment.
Première étape on invisibilise la tâche de release-prod
en la suffixant d'un point .release-prod
.
.release-prod:
image: rust:1.77
stage: packaging
# seulement si c'est main
only:
- main
script:
- *prepare
- *packaging
Puis on utilise la composition et matrix.
release-prod:
extends: .release-prod
when: manual
parallel:
matrix:
- CRATE_PACKAGE: w1
- CRATE_PACKAGE: w2
- CRATE_PACKAGE: w3
Nous avons maintenant un pipeline qui permet de créer de manière indépendante des versions de releases.
Récupérer la version
Mais il reste une question en suspend.
Comment récupérer le nom et la version du package ciblé, pour le nom on l'a déjà mais nous allons tout de même le reconstruire pour être certain.
Pour faire cela, nous allons utiliser la commande cargo metadata
pour lister les packages existants:
|
Nous obtenons ce json pour un workspace
Et celui-ci pour un projet "simple"
Nous avons donc 2 cas à traiter:
- Le premier on cherche à récupérer la version d'un package explicitement nommé par la variable
$CRATE_PACKAGE
- Le second est le cas implicite d'un état à un package et l'absence de variable
$CRATE_PACKAGE
Comment procède-t-on ?
Et bien jq est bien plus qu'il ne semble être.
Il est par exemple possible de créer des conditions qui vont venir créer des branches d'exécutions.
Notre algorithm va être:
SI la "taille du tableau de package" est supérieur à 1
ALORS
SELECTIONNER le package qui possède le nom == $CRATE_PACKAGE
SINON
SELECTIONNER le premier package
END
En jq cela nous donne
if (.packages | length ) > 1
then
(.packages[] | select(.name == $PACKAGE))
else
.packages[0]
end
Puis nous pouvons récupérer ce qui nous intéresse
{"name" : .name, "version": .version, "manifest": .manifest_path}
On export le résultat dans une variable $PACKAGE_DATA
Ensuite, nous mettons en place un garde-fou, qui a deux rôle:
- vérifier que au moins un package existe dans le workspace
- vérifier que le package sélectionné existe
Pour cela on vérifie que la variable n'est pas vide sinon on quitte
[ -z "$PACKAGE_DATA" ] && echo "Unknown package $PACKAGE in workspace" && exit 1
Ensuite, on récupère tranquillement:
- la version
- le Cargo.toml du package
- le nom du package
Dépendances transitives
Mais il y a un autre souci, un de plus 😝
Pour créer les versions flottantes, nous modifions le Cargo.toml
du package voulu
Mais ça cargo n'aime pas du tout, si dans ses dépendences il y a
[]
= "w2"
[]
= {="../w1", = "0.2.0", ="private-crates"}
Et que l'on s'amuse à transformer la version du package w1
[]
= "w1"
= "0.2.0-124eba69"
Cargo va pas être content car il ne s'attend pas à cette version et n'a plus de repère pour créer le package
if you are looking for the prerelease package it needs to be specified explicitly
w1 = { version = "0.2.0-124eba69" }
Il faut donc aller trafiquer le Cargo.toml de w2
pour lui donner la bonne version.
[]
= "w2"
[]
# dynamiquement modifié
= {="../w1", = "0.2.0-124eba69", ="private-crates"}
Et ce, pour toutes les packages utilisant w1
dans le workspace.
Ok, on fait ça comment ?
La manière la plus "simple" est de lister les 'Cargo.toml'
|
En suite on liste en suite les dépendences qui possède w1
.
|
Renvera
Par contre
|
Renvera
null
Il est ainsi possible de fail fast sur les Cargo.toml qui ne nous intéressent pas.
Ensuite il suffit de réaliser le remplacement dans le toml
Ce serait si simple si cela marchait aussi facilement mais il y a un soucis
Lorque que l'on tente de variabiliser
On arrive sur cette erreur
jq: error: syntax error, unexpected '$', expecting FORMAT or QQSTRING_START (Unix shell quoting issues?) at <top-level>, line 1:
.dependencies.$PACKAGE.version = "0.2.0-124eba69
La solution de déport que j'ai trouvé est de templatiser l'expression à coup de bash expansion
FILE=w2/Cargo.toml
PACKAGE=w1
VERSION=0.2.0-124eba69
COMMAND=
Et cette fois-ci c'est fonctionnel !
[]
= "w2"
[]
= {="../w1", = "0.2.0-124eba69", ="private-crates"}
On se rapproche, maintenant, nous voulons appliquer cette modification à tous les sous package.
Nous sommes capable de les lister, plus qu'à appliquer.
Et pour ça il nous faut un autre outil qui se nomme xargs
, c'est une usine à gaz, donc je ne vais pas rentrer dans les détails mais l'idée ici c'est qu'il va prendre les résultat ligne par ligne et les appliquer à des traitements.
Par exemple
|
Cela affichera
titi+suffix
tata+suffix
tutu+suffix
Si vous êtes sur windows wsl, attention a bien créer le fichier sans
CRLF
, xargs ne prend en compte que leLF
et traite leCR
comme un caractère indifférent.
On vient créer un script à coup de heredoc
Et finalement on rassemble tout !
| |
stages:
- packaging
variables:
# gitlab token
CRATE_PACKAGE_TOKEN: $CI_JOB_TOKEN
# l'ID du projet qui supportera les crates par défaut le project du job
CRATE_PACKAGE_PROJECT_ID: $CI_PROJECT_ID
# API user
CRATE_PACKAGE_USER_API: JOB-TOKEN
# Host du proxy SSH
CRATE_PACKAGE_ENDPOINT: noa-crates.cleverapps.io
# Port d'écoute du proxy SSH
CRATE_PACKAGE_PORT: 22066
# Utilisateur associé au token
CRATE_PACKAGE_USER: personal-token
# Package ciblé
CRATE_PACKAGE: ""
.dependencies:
# dépendences
- apt update && apt install -y yq
.ssh-connexion:
# configuration SSH
- mkdir -p ~/.ssh && chmod -R 700 ~/.ssh
- |
cat << EOF > ~/.ssh/config
Host $CRATE_PACKAGE_ENDPOINT
User $CRATE_PACKAGE_USER:$CRATE_PACKAGE_TOKEN
Port $CRATE_PACKAGE_PORT
EOF
# création de la paire de clefs
- ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519
# ajout de la clef publique du proxy
- ssh-keyscan -p $CRATE_PACKAGE_PORT $CRATE_PACKAGE_ENDPOINT >> ~/.ssh/known_hosts
.packaging:
# packaging
- cargo package -p $PACKAGE --allow-dirty
- cargo metadata --format-version 1 > metadata.json
- ls target/package/$CRATE_FILE
# upload
- 'curl -i --header "$CRATE_PACKAGE_USER_API: $CRATE_PACKAGE_TOKEN" --upload-file target/package/${CRATE_FILE} "${CI_API_V4_URL}/projects/${CRATE_PACKAGE_PROJECT_ID}/packages/generic/${CRATE_NAME}/${CRATE_VERSION}/$CRATE_FILE"'
- 'curl -i --header "$CRATE_PACKAGE_USER_API: $CRATE_PACKAGE_TOKEN" --upload-file metadata.json "${CI_API_V4_URL}/projects/${CRATE_PACKAGE_PROJECT_ID}/packages/generic/${CRATE_NAME}/${CRATE_VERSION}/metadata.json"'
.version:
# récupération des informations du paquet
- |
export PACKAGE_DATA=$(cargo metadata --format-version 1 --no-deps | jq --arg PACKAGE "$PACKAGE" '(if (.packages | length ) > 1 then (.packages[] | select(.name == $PACKAGE)) else .packages[0] end) | {"name" : .name, "version": .version, "manifest": .manifest_path}')
# on quitte si le package n'existe
- '[ -z "$PACKAGE_DATA" ] && echo "Unknown package $PACKAGE in workspace" && exit 1'
- export CRATE_NAME=$(jq ".name" <<< "$PACKAGE_DATA" | tr -d '"')
- export CRATE_VERSION=$(jq ".version" <<< "$PACKAGE_DATA" | tr -d '"')
- export CARGO_FILE=$(jq ".manifest" <<< "$PACKAGE_DATA" | tr -d '"')
- export CRATE_FILE=${CRATE_NAME}-${CRATE_VERSION}.crate
.prepare:
- *dependencies
- *ssh-connexion
- *version
.release-dev:
image: rust:1.77
stage: packaging
# manuel
when: manual
# si la branche n'est pas main
rules:
- if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
script:
- *prepare
# on créé la version flottante
- export CRATE_VERSION=$CRATE_VERSION-$CI_COMMIT_SHORT_SHA
- export CRATE_FILE=${CRATE_NAME}-${CRATE_VERSION}.crate
- |
cat << 'EOF' > ~/upgrade-cargo.sh
FILE=$1
PACKAGE=$2
VERSION=$3
result=$([ "null" != "$(tomlq '.dependencies' "$FILE" | jq ."$PACKAGE")" ] && tomlq -t ".dependencies.$PACKAGE.version = \"$VERSION\"" "$FILE")
[ -n "$result" ] && echo "$result" > "$FILE"
exit 0
EOF
# On remplace la version
- |
cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | .manifest_path | tostring' | xargs -I {} bash ~/upgrade-cargo.sh {} $PACKAGE $CRATE_VERSION
- tomlq --arg VERSION $CRATE_VERSION -t '.package.version = $VERSION' $CARGO_FILE > Cargo.toml.modified
- mv Cargo.toml.modified $CARGO_FILE
- *packaging
.release-prod:
image: rust:1.77
stage: packaging
# seulement si c'est main
only:
- main
script:
- *prepare
- *packaging
release-dev:
extends: .release-dev
when: manual
parallel:
matrix:
- PACKAGE: w1
- PACKAGE: w2
- PACKAGE: w3
release-prod:
extends: .release-prod
when: manual
parallel:
matrix:
- PACKAGE: w1
- PACKAGE: w2
- PACKAGE: w3
Ce travail est sous licence CC BY-NC-SA 4.0.