Partie 3: Gestion des workspaces
Lecture 5 min. âą
Table des matiĂšres
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.
echo [workspace] > Cargo.toml
cargo new --lib w1Une ligne va venir se rajouter dans le Cargo.toml
Cargo.toml
[workspace]
members = ["w1"]Et on peut rajouter autant de sous crates que lâon veut
cargo new --lib w2
cargo new --lib w3Si lâon tente de faire
cargo package --allow-dirtyIl 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
cargo package -p w1 --allow-dirtyParfait 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
cargo metadata --format-version 1 --no-deps > metadata.jsonOn va donc utiliser une variable CRATE_PACKAGE
variables:
CRATE_PACKAGE: ""Qui va venir contrÎler le package à créer.
cargo package -p $CRATE_PACKAGE --allow-dirtyOk, 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
- *packagingPuis on utilise la composition et matrix.
release-prod:
extends: .release-prod
when: manual
parallel:
matrix:
- CRATE_PACKAGE: w1
- CRATE_PACKAGE: w2
- CRATE_PACKAGE: w3Nous 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:
cargo metadata --format-version 1 --no-deps | jqNous obtenons ce json pour un workspace
{
"packages" : [
{
"name" : "w1",
"version" : "0.1.0",
"manifest_path" : "/path/to/workspace/w1/Cargo.toml"
},
{
"name" : "w2",
"version" : "0.1.0",
"manifest_path" : "/path/to/workspace/w2/Cargo.toml"
},
{
"name" : "w3",
"version" : "0.1.0",
"manifest_path" : "/path/to/workspace/w3/Cargo.toml"
}
],
// ...
}Et celui-ci pour un projet âsimpleâ
{
"packages" : [
{
"name" : "subber",
"version" : "0.1.0",
"manifest_path" : "/path/to/subber/Cargo.toml"
},
],
// ...
}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
ENDEn jq cela nous donne
if (.packages | length ) > 1
then
(.packages[] | select(.name == $PACKAGE))
else
.packages[0]
endPuis 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
export PACKAGE_DATA=$(cargo metadata --format-version 1 --no-deps | jq --arg PACKAGE "$CRATE_PACKAGE" '(if (.packages | length ) > 1 then (.packages[] | select(.name == $PACKAGE)) else .packages[0] end) | {"name" : .name, "version": .version, "manifest": .manifest_path}')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 1Ensuite, on récupÚre tranquillement:
- la version
- le Cargo.toml du package
- le nom du package
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 '"')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/Cargo.toml
[package]
name = "w2"
[dependencies]
w1 = {path="../w1", version = "0.2.0", registry="private-crates"}Et que lâon sâamuse Ă transformer la version du package w1
w2/Cargo.toml
[package]
name = "w1"
version = "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/Cargo.toml
[package]
name = "w2"
[dependencies]
# dynamiquement modifié
w1 = {path="../w1", version = "0.2.0-124eba69", registry="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â
cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | .manifest_path | tostringEn suite on liste en suite les dépendences qui possÚde w1.
tomlq '.dependencies' w2/Cargo.toml | jq ."w1"Renvera
{
"path": "../w1",
"version": "0.1.0-toto"
}Par contre
tomlq '.dependencies' w1/Cargo.toml | jq ."w1"Renvera
nullIl 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
tomlq -t '.dependencies.w1.version = "0.2.0-124eba69"' w2/Cargo.tomlCe serait si simple si cela marchait aussi facilement mais il y a un soucis
Lorque que lâon tente de variabiliser
tomlq --arg PAKAGE w1 -t ".dependencies.$PACKAGE.version = "0.2.0-124eba69"" w2/Cargo.tomlOn 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-124eba69La 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=$(tomlq -t ".dependencies.$PACKAGE.version = \"$VERSION\"" "$FILE")
echo "$COMMAND"Et cette fois-ci câest fonctionnel !
[package]
name = "w2"
[dependencies]
w1 = {path="../w1", version = "0.2.0-124eba69", registry="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
$ cat toto
titi
tata
tutu
$ cat toto | xargs -I {} echo {}+suffixCela affichera
titi+suffix
tata+suffix
tutu+suffixSi vous ĂȘtes sur windows wsl, attention a bien crĂ©er le fichier sans
CRLF, xargs ne prend en compte que leLFet traite leCRcomme un caractÚre indifférent.
On vient créer un script à coup de heredoc
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
EOFEt finalement on rassemble tout !
cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | .manifest_path | tostring' | xargs -I {} bash ~/upgrade-cargo.sh {} $PACKAGE $CRATE_VERSION.gitlab-ci.yml
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: &dependencies
# dépendences
- apt update && apt install -y yq
.ssh-connexion: &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
# 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: &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: &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