7.7 KiB
layout | title | date | status | sitemap | category | description |
---|---|---|---|---|---|---|
post | Gérer ses artefacts de build sans daemon | 2023-04-12T12:06:46.134+02:00 | draft | true | developpement | Comme vous pouvez le voir, on aime rester léger, et s'embarasser le moins possible de daemons. |
Cet article est la suite de mon précédent article Un registre statique avec Garage.
Le premier point que j'ai regardé à la suite de la lecture de cet article était de savoir si on pouvait utiliser le module ociTools
de NixOS pour générer des images directement comme je voulais via nix build
. Il apparait que cet outil ne génère pas des images mais des conteneurs, deux représentations différentes, que je ne sais pas comment convertir en plus. Bref, ça ne nous avance pas, et si on veut faire mieux, je crains qu'il va simplement falloir écrire notre propre module NixOS, ce qui est trop couteux en temps pour le moment : on se contentera de cette innéficacité pour le moment…
Ensuite, il est bon de noter qu'on a déjà une logique pour lister les builds statics en place écrite en nixlang pour Garage dans un fichier nommé build_index.nix. En gros elle va reprendre le concept de “tags” de Docker, et à l'aide d'une regex sur le tag, elle va classer les builds en 3 catégories : les builds de release, qui correspondent à un tag semver sans libellé (eg. 0.8.2), les builds extra, qui correspondent à un semver avec libellé (eg. 0.8.2+feat), et enfin tous les autres sont des builds de développement.
On peut alors imaginer un affichage différencié de ces buils : les builds release sont affichés par défaut, les autres sont cachés derrière un bouton. On peut aussi imaginer une gestion des cycles de vie différents : les builds release sont gardés pour toujours, les autres sont supprimés après un certain temps.
L'API S3 a un concept de Lifecycle Management et peut expirer des objets après un temps donné, c'est à dire les supprimer. Par exemple, on peut définir que les objets ayant leur préfix commençant par logs/
seront supprimés après un an. Il serait tentant d'utiliser ce système pour notre système de gestion d'artefacts mais ça pose plusieurs problèmes : à ce jour, Garage n'implémente pas les Lifecycle Management, aussi le fonctionnement des images Docker fait qu'il est possible que plusieurs version d'une même image partagent un même blob, et enfin on est amené à générer un index qui liste toutes nos images, et qui se retrouvera donc à être invalide.
Docker, et donc l'Open Container Initiative, a une spécification pour définir un index de tags pour une image donnée. Tout ça est décrit dans la spec “distribution” dans la section Content Discovery. Implémenter cette spécification pour lister nos tags améliorerait l'intéropérabilité de notre registre avec les autres clients OCI. Même si on ne pourra pas tout implémenter, par exemple le paging définit par la spec ne pourra pas être codé.
La spécification OCI pourrait nous servir d'inspiration pour notre dépôt de fichiers statiques. Tout ça dans l'espoir de faciliter l'écriture du code et de limiter le nombre de conceps à manipuler. Mais cela veut aussi dire qu'on extrait de l'index la charge de catégoriser les tags (release, extra, development). Ça deviendrait alors une convention pour celles et ceux qui intéragissent avec notre futur outil. Et ça veut dire que si on ne garbage collect pas les nightly builds, cet index peut devenir bien grand…
À travers ce panorama, je pense qu'on arrive au noeud du problème à traiter aujourd'hui : la spécification de notre index pour les fichiers statiques, la spécification de la convention à suivre pour les tags, et comment créer un outil qui facilite au maximum cette gestion.
Créer un index pour un registre OCI
On peut regarder un peu ce que donne le registre Docker pour Garage :
{
"name": "dxflrs/garage",
"tags": [
"02e8eb167efa1f08d69fe7f8e6192cde726c45aa",
"<skipped entries>",
"fcc5033466e58e3beec05ee7748d33522b6b32b0",
"v0.7.3",
"v0.8-rc2",
"v0.8.0",
"v0.8.0-beta1",
"v0.8.0-beta2",
"v0.8.0-rc1",
"v0.8.0-rc2",
"v0.8.1",
"v0.8.2"
]
}
On peut reproduire le même contenu sur notre registre en commençant par récupérer tous les manifests qui ne commencent pas par sha256 :
aws s3 ls s3://registry.deuxfleurs.org/v2/albatros/manifests/ | \
tr -s ' ' | \
cut -d' ' -f 4 | \
grep -v '^sha256:'
# 0.9
On peut construire le JSON à la main pour cette fois :
{
"name": "albatros",
"tags": [
"0.9"
]
}
L'envoyer :
aws s3 cp /tmp/tags.json s3://registry.deuxfleurs.org/v2/albatros/tags/list
Et ensuite s'assurer qu'on est bien compatible :
crane ls registry.deuxfleurs.org/albatros
# 0.9
À ma connaissance, le client Docker ne permet pas de récupérer ce fichier directement.
Bien entendu, il est peu probable que dégainer l'outil crane
soit la première solution qui vienne à l'esprit des gens. Nos utilisateurs vont plutôt se rendre sur notre site web et s'attendre à voir les conteneurs listés sur une page dédiée ! En définissant des CORS sur notre bucket, on va permettre à un script JS astucieux de récupérer ce fichier via des requêtes XHR et de construire l'index.
On pourrait aussi appeler ces fichiers depuis le générateur de site statique, et régulièrement regénérer le site web pour intégrer ces modifications. Aujourd'hui on utilise XHR sur la page de Garage, et ça a quand même l'avantage de la simplicité, alors pas de raison de changer :-)
On va configurer les CORS de sorte que n'importe qui puisse lire notre index. On réalise ce choix car on ne peut pas spécifier de sous-domaine autorisé : c'est tout ou rien. Et vu qu'on ne sait pas par avance tous les sites webs qui pourraient vouloir requêter notre registre, on autorise tout le monde. Par contre on autorise seulement le GET, et voilà ce que ça donne au final :
export CORS='{"CORSRules":[{"AllowedHeaders":["*"],"AllowedMethods":["GET"],"AllowedOrigins":["*"]}]}'
aws s3api put-bucket-cors --bucket registry.deuxfleurs.org --cors-configuration $CORS
Voilà, maintenant on a tout pour générer une page web.
Générer une page web pour nos releases
On peut commencer simplement avec un squelette basique :
e<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>reg</title>
</head>
<body>
<pre>list</pre>
<script type="text/javascript">
console.log("hello world")
</script>
</body>
</html>
On va avoir besoin d'une API du navigateur nommée FileReader
mais cette dernière utilise la sémantique des callbacks. On va créer un wrapper avec des Promises pour simplifier son utilisation :
const reader = blob => {
const tmp = new FileReader();
return new Promise((resolve, reject) => {
tmp.onerror = () => {
tmp.abort()
reject(new DOMException("Problem parsing blob"))
}
tmp.onload = () => {
resolve(tmp.result)
}
tmp.readAsText(blob)
})
}
Et maintenant on peut simplement écrire notre logique pour récupérer la liste des tags :
const albatros_tags = async () => {
const res = await fetch('https://registry.deuxfleurs.org/v2/albatros/tags/list')
const blob = await res.blob()
const txt = await reader(blob)
const tags = JSON.parse(txt)
return tags
}
(async () => console.log(await albatros_tags()))()
Ensuite il ne reste plus qu'à l'insérer dans le DOM de la page :