Partie 1: Présentation, Installation du Proxy et action manuelle
Bonjour à toutes et à tous 😀
crates.io, c'est vraiment bien, c'est une plateforme qui unifie toutes les crates publiques en un seul point.
Mais il est là le souci, crates publiques, dès que l'on veut avoir des crates privées, cela devient plus complexe à cause de l'authentification nécessaire.
Cargo permet au moyen d'un fichier addon .cargo/config.toml
, à définir au niveau du projet où du système entier, de spécifier des registries privées en utilisant la nomenclature
[]
= "Endpoint URL"
Et peut alors s'utiliser dans un Cargo.toml
comme n'importe quelle crate.
[]
#...
[]
= { = "1.0.0", = "my-private-registry" }
Et si en plus d'être privée, votre registry nécessite de l'authentification alors vous devez vous bâtir votre propre système de credential provider.
Il existe diverses solutions comme artifactory, kellnr ou s'installer son propre crates.io qui tendent à rapprocher l'expérience de dev la plus proche d'un crates.io
classique.
Toutes ces solutions sont cools, mais nécessitent de la maintenance et moi, je suis fainéant. Je ne veux pas à avoir à setup un autre bidule contraignant.
C'est alors que j'ai découvert une solution des plus élégantes, il s'agit de gitlab-cargo-shim, un projet sous licence WTFPL qui fait le juste travail que je lui demande.
Son principe de fonctionnement est fort simple. Gitlab dispose (même en version SaaS) d'une package registry generic. Celle-ci permet au moyen d'appels API authentifiés par token d'accès (personnel, de groupe, de projet, de CI) de venir pousser et télécharger de la données dans un bucket S3.
L'idée est donc de mettre à profit cette API de package registry, pour venir y stocker les fichiers .crate qui sont basiquement des archives de projets.
Profitons de ce backend déjà existant 😄
Le problème est
Comment faire parler cargo en API Gitlab?
Et comme généralement dans ces cas de transitions de protocoles la réponse est : un proxy!
Nous avons donc un proxy qui parle à la fois le "cargo" et le "gitlab" et gitlab parle à son bucket S3.
+--------+ +-------+ +--------+ +----+
| Cargo <-----> Proxy <---> Gitlab <----> S3 |
+--------+ +-------+ +--------+ +----+
Deuxième problème
Comment authentifier les appels de cargo vers Gitlab?
En effet, pour que le proxy puisse intéragir avec la package registry via un appel API, celui-ci doit comporter le token d'authentification.
Il faut donc réussir à réaliser ceci.
┌───────┐ Token ┌───────┐ Token ┌────────┐
│ Cargo ├──────────► Proxy ├─────────► Gitlab │
└───────┘ ??? └───────┘ API └────────┘
HTTPS
La question que l'on doit alors se poser c'est qu'est que l'on met à la place des ???
.
Cargo parle plusieurs protocoles:
- git
- ssh
- https
Nous n'allons pas utiliser git car c'est un protocole trop spécialisé pour ce que l'on a besoin de faire, nous ne pouvons pas non plus utiliser https car l'ajout mot de passe dans une URL est très peu recommandé pour des raisons évidentes de sécurité.
Il nous reste SSH.
Mais le problème reste entier, comment transmettre de manière sécurisée le token d'authentification pour l'API Gitlab ?
Et c'est là qu'on peut être extrêment malin.
Lorsque l'on se connecte au travers de SSH on le fait toujours au travers d'un utilisateur, et le format du nom de l'utilisateur n'a pas vraiment de formalisme.
L'idée est donc de faire pareil qu'avec le https.
Mais au lieu de le mettre dans l'URL en clair c'est le protocole SSH qui le transmettra de manière sécurisée une fois le handshake réalisé.
[]
= "ssh://personal-token:[TOKEN]@[ENDPOINT]"
Sur le papier c'est bien, sauf que les créateur de cargo n'ont pas fais dans la dentelles sur l'interdiction des mots de passes dans l'URL d'une registry.
Mais c'est là que l'on peut être doublement malin.
SSH vient avec un fichier qui se nomme .ssh/config
.
Dedans, il est possible par host de définir des règles à exécuter. Nous tout ce que l'on veut c'est que l'URL reste immaculé de mot de passe mais qu'il soit tout de même transmit.
Le fichier de config à ce pouvoir
Host ENDPOINT
User personal-token:TOKEN
Et voilà ! Maintenant le token arrive jusqu'au proxy ! Qui peut alors réaliser en délégation de droits, les calls API sur Gitlab et transmettre les réponses dans le stream SSH.
┌───────┐ Token ┌───────┐ Token ┌────────┐ ┌────┐
│ Cargo ◄──────────► Proxy ◄─────────► Gitlab ◄────► S3 │
└───────┘ SSH └───────┘ API └────────┘ └────┘
HTTPS
Il nous faut maintenant le proxy.
Vous pouvez très bien le faire tourner en local mais j'ai décider de l'héberger chez clever-cloud car cela sera plus pratique pour la suite.
Commençons.
Installons les clever-tools.
npm i -g clever-tools
Je vous conseil une version node 20 d'installé sinon il va râler.
On s'authentifie
clever login
Puis on créé une app une application nodeJS, oui c'est chelou mais comme notre proxy ne parle que SSH, lorsque l'on va venir vérifier si le port HTTP 8080 répond, ça ne marchera pas.
On va donc ruser un peu.
On clone notre proxy
git clone https://github.com/w4/gitlab-cargo-shim.git && gitlab-cargo-shim
Puis on crée l'application
clever create --type node
Your application has been successfully created!
ID: app_bf53be9f-abbc-421d-9a43-c097cfc476e9
Il est possible de définir l'organisation de l'application avec le paramètre
--org
.
On lie l'application et le dépôt avec
clever link app_bf53be9f-abbc-421d-9a43-c097cfc476e9
On se met dans une branche
git branch clever
git switch clever
On créé un fichier à la racine du projet package.json
avec le contenu suivant.
On commit.
Sur la console on vient rajouter plusieurs variable d'environement:
D'abord CONFIG
avec le contenu suivant
## socket address for the SSH server to listen on
= "[::]:4040"
## directory in which the generated private keys for the server
## should be stored
= "/home/bas/state"
[]
## the base url of the gitlab instance
= "https://gitlab.com"
- Le port
4040
est obligatoire. - Le
/home/bas/state
permet d'avoir un endroit où l'application a les droits en écritures. - L'
uri
est à votre convenance, moi je vais utiliser l'instance SaaS de Gitlab mais vous êtes libre d'utiliser votre instance privée.
Puis qui créé la configuration et lance le binaire
CC_RUN_COMMAND = echo "${CONFIG}" > /home/bas/config.toml && target/release/gitlab-cargo-shim --config /home/bas/config.toml
Qui démarre un serveur HTTP sur le port 8080 en background et répond "OK"
CC_PRE_RUN_HOOK = mkdir -p dist && echo OK > dist/index.html && (python3 -m http.server -d dist 8080)&
Et enfin qui va cacher uniquement le package.json
et le binaire à l'issu de l'étape de build, passant de 1Go à 17Mo le cache 😁
CC_OVERRIDE_BUILDCACHE = /package.json:/target/release/gitlab-cargo-shim
Tout l'env en un clic
CC_OVERRIDE_BUILDCACHE="/package.json:/target/release/gitlab-cargo-shim"
CC_PRE_RUN_HOOK="mkdir -p dist && echo OK > dist/index.html && (python3 -m http.server -d dist 8080)&"
CC_RUN_COMMAND="echo \"${CONFIG}\" > /home/bas/config.toml && target/release/gitlab-cargo-shim --config /home/bas/config.toml"
CONFIG="## socket address for the SSH server to listen on
listen-address = \"[::]:4040\"
## directory in which the generated private keys for the server
## should be stored
state-directory = \"/home/bas/state\"
[gitlab]
## the base url of the gitlab instance
uri = \"https://gitlab.com\""
Je vous conseille également d'activer l'utilisation d'une instance de build dans Information -> Enable dedicated build instance, et de la passer en XL si vous ne voulez pas mourrir d'ennuie en attendant le build.
Votre instance de run peut rester une XS.
Vous pouvez également lui ajouter un domain perso. Moi ça sera noa-crates.cleverapps.io
.
Finalement activez la redirection tcp
clever tcp-redirs add --namespace cleverapps
Vous allez obtenir un numéro de port, c'est celui-ci qui sera redirigé vers le port 4040 de notre proxy SSH.
Successfully added tcp redirection on port: 22066
On configure le .ssh/config
.
Host noa-crates.cleverapps.io
User personal-token:TOKEN
Port 22066
Vous pouvez maintenant déployer
clever deploy
Après plusieurs minutes de build, votre app est up and running et prête à recevoir du SSH.
ssh noa-crates.cleverapps.io
Cela prend un certain temps et non ce n'est pas encore la faille xz qui ralenti la connexion, mais simplement le trajet très étendu qui fait un call sur Gitlab.
En effet voici la réponse
Hi there, Akanoa! You've successfully authenticated, but gitlab-cargo-shim does not provide shell access.
Connection to noa-crates.cleverapps.io closed.
Le Akanoa
est une information connu de seul Gitlab, ce qui prouve à la fois que le SSH répond mais aussi que le proxy utilise bel et bien mon token pour opérer sur Gitlab!! 🤩
Et donc comment pouvons nous utiliser notre système avec cargo ?
Déjà nous ne ferons pas de cargo publish
, il est trop opiniated et va nous géner.
Par contre, nous pouvons décomposer ses étapes pour créer notre flux de commandes.
Testons à partir d'un projet vide.
cargo init project --lib && cd project
On lance d'abord un
cargo package --allow-dirty
--allow-dirty
permet de ne pas à avoir à committer les modifications
Cela créé un fichier target/package/project-0.1.0.crate
.
Dedans on y trouve seulement une archive de notre projet.
tar -tvf target\package\project-0.1.0.crate
-rw-r--r-- 0 0 0 545 janv. 01 1970 project-0.1.0/Cargo.toml
-rw-r--r-- 0 0 0 78 nov. 29 1973 project-0.1.0/Cargo.toml.orig
-rw-r--r-- 0 0 0 216 nov. 29 1973 project-0.1.0/src/lib.rs
On génère également un manifeste qui contient tout de sorte de métadonnées.
cargo metadata --format-version 1 > metadata.json
Je ne le mets pas en entier ici, mais il contient la résolution de l'arbre de dépendances du projet et d'autres informations dont une représentation du Cargo.toml dans notre archive.
Ok, et maintenant, on fait comment pour l'envoyer sur Gitlab ?
Et bien c'est simple: curl
😄 Et je ne déconne même pas. Nous allons utiliser l'API de Gitlab comme elle doit être utilisée.
Mais d'abord, il nous faut un projet.
On en créé un sur Gitlab et on récupère le project ID qui se trouve dans les 3 points verticaux en haut à droite ⋮
.
Ici ça sera 57111774.
Vérifiez bien que dans les paramètres du projet dans
/settings/packages_and_registries
, que le "Number of duplicate assets to keep" est bien à 1 !
TOKEN=[votre token]
PROJECT_ID=57111774
CRATE_NAME=project
CRATE_VERSION=0.1.0
CRATE_FILE=
- .crateENDPOINT=https://gitlab.com/api/v4
Ici cela donne
curl --header 'PRIVATE-TOKEN: ***REDACTED***' --upload-file target/package/project-0.1.0 https://gitlab.com/api/v4/projects/57111774/packages/generic/project/0.1.0/project-0.1.0.crate
curl --header 'PRIVATE-TOKEN: ***REDACTED***' --upload-file metadata.json https://gitlab.com/api/v4/projects/57111774/packages/generic/project/0.1.0/metadata.json
Si vous vous dirigez dans la package registry
du projet vous verrez que le paquet project-0.1.0
est bien présent et dedans vous pourrez apercevoir le project-0.1.0.crate
.
Et maintenant comment ça s'utilise dans un projet qui a bien de project-0.1.0
?
Pas du curl quand même ???
Non, non, on va enfin utiliser le proxy ^^
On se créé un nouveau projet
cargo init need_project
Puis on créé le
[]
= "ssh://[proxy endpoint]/[path project]"
[]
= true
Attention à ne pas oublier le
net.git-fetch-with-cli = true
, sinon rien ne marche !!
Pour moi proxy endpoint
sera "noa-crates.cleverapps.io". Il faut simplement qu'il corresponde au HostName de votre .ssh/config
.
Le path project
est celui de votre projet gitlab qui possède la package registry. Moi ça sera "noa-crates/project".
Ce qui me donne
[]
= "ssh://noa-crates.cleverapps.io/noa-crates/project"
[]
= true
Ensuite on utilise la dépendence classiquement
[]
# ...
[]
= {="0.1.0", ="private-crate"}
Si on build
cargo build
Compiling project v0.1.0 (registry `private-crate`)
Compiling need-project v0.1.0 (/data/need-project)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s
On voit que la dépendence project v0.1.0
a bien été tirée.
Notre package registry fonctionne 😁
Dans le prochain article on verra comment industrialiser tout ça ^^
Ce travail est sous licence CC BY-NC-SA 4.0.