https://lafor.ge/feed.xml

Un crates.io sur Gitlab

2024-04-21

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

.cargo/config.toml
[registries.my-private-registry]
index = "Endpoint URL"

Et peut alors s'utiliser dans un Cargo.toml comme n'importe quelle crate.

Cargo.toml
[package]
#...

[dependencies]
private-crate = {version = "1.0.0", registry = "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é.

.cargo/config.toml
[registries.my-private-registry]
index = "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

.ssh/config
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.

{
    "name": "app",
    "scripts" : {
        "install": "cargo build --release"
    }
}

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
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"
  • 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.

TOKEN=[votre token]
PROJECT_ID=57111774
CRATE_NAME=project
CRATE_VERSION=0.1.0
CRATE_FILE=$CRATE_NAME-$CRATE_VERSION.crate
ENDPOINT=https://gitlab.com/api/v4
curl --header "PRIVATE-TOKEN: $TOKEN" --upload-file target/package/${CRATE_FILE} "${ENDPOINT}/projects/${PROJECT_ID}/packages/generic/${CRATE_NAME}/${CRATE_VERSION}/$CRATE_FILE"
curl --header "PRIVATE-TOKEN: $TOKEN" --upload-file metadata.json "${ENDPOINT}/projects/${PROJECT_ID}/packages/generic/${CRATE_NAME}/${CRATE_VERSION}/metadata.json"

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

.cargo/config
[registries.private-crate]
index = "ssh://[proxy endpoint]/[path project]"
[net]
git-fetch-with-cli = 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

.cargo/config
[registries.private-crate]
index = "ssh://noa-crates.cleverapps.io/noa-crates/project"
[net]
git-fetch-with-cli = true

Ensuite on utilise la dépendence classiquement

.cargo/config.toml
[package]
# ...

[dependencies]
project = {version="0.1.0", registry="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 ^^

avatar

Auteur: Akanoa

Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.

Ce travail est sous licence CC BY-NC-SA 4.0.