Appliquer les migrations de la base
Vous devez amener la base d’une passerelle déjà déployée au schéma courant. Cette page décrit le chemin, et surtout la migration qui ne se défait pas.
À lire avant tout : deux pièges qui coûtent des clés
Section intitulée « À lire avant tout : deux pièges qui coûtent des clés »La liste des migrations ne se lit dans aucun document
Section intitulée « La liste des migrations ne se lit dans aucun document »Un document intitulé docs/specs/gateway-db-schema.md a décrit la chaîne comme
s’arrêtant à la migration 009. Ce fichier ne fait plus partie du dépôt, mais
gateway-db/README.md y renvoie encore, et des copies circulent.
Le lire comme un état des lieux est dangereux : le script applique aujourd’hui des
migrations postérieures à 009, dont une irréversible qui supprime le stockage
des clés en clair. Quelqu’un qui suit ce document croit poser un renommage de
contrainte, et supprime en réalité une colonne dont rien ne le ramènera.
La seule liste qui fait foi est celle que le script imprime, parce qu’elle est calculée sur les fichiers réellement présents et sur ce que la base a déjà reçu :
export DATABASE_URL="postgresql://<utilisateur>:<mot-de-passe>@<hôte>:<port>/<base>"cd gateway-db./migrate.sh --statusChaque ligne [ ] est une migration qui sera exécutée au prochain appel sans
option. Lisez-les toutes avant de lancer quoi que ce soit.
Une migration s’applique sur une sauvegarde qu’on sait restaurer
Section intitulée « Une migration s’applique sur une sauvegarde qu’on sait restaurer »L’étape suivante en dépend, alors autant le poser tout de suite : prendre une sauvegarde ne suffit pas. Ce qui protège, c’est d’avoir déjà restauré une sauvegarde au moins une fois, à froid, sur cette installation-là. Tant que l’aller-retour n’a pas été fait, vous avez des fichiers dont personne ne sait s’ils remontent.
La procédure — les deux scripts, l’inventaire à comparer avant et après, et l’essai de restauration sur une base de côté — est dans Sauvegarder et restaurer la base de la passerelle. Faites-la avant de revenir ici.
Deux points qu’elle détaille et qui pèsent sur ce qui suit :
- la sauvegarde ne porte pas les secrets qui vivent hors de la base — les clés de
vos fournisseurs, dont la base ne connaît que le nom de la variable d’environnement,
ni
ADMIN_API_KEY. Une base restaurée ne fait pas une installation qui repart ; - le fichier est un secret : avant la migration
015, il contient les clés de tous vos porteurs en clair.
Les instantanés gérés hors dépôt par votre hébergeur restent utiles, mais ils ne remplacent pas cet essai : ni leur cadence, ni leur rétention, ni leur contenu exact ne sont vérifiables depuis ici.
Les migrations qui existent aujourd’hui
Section intitulée « Les migrations qui existent aujourd’hui »Le fichier fait foi ; ce tableau sert à reconnaître ce que le script annonce.
| Migration | Ce qu’elle fait |
|---|---|
000-initial |
ligne de base — reproduit l’état d’avant 001, défauts compris |
001-endpoints |
table endpoints, identifiant numérique sur providers |
002-endpoint-enabled |
colonne enabled sur les endpoints |
003-model-table |
table models ; les endpoints désignent un modèle plutôt qu’une chaîne |
004-plans |
table plans et rattachement des comptes — naissance de la facturation |
005-convertion-rate |
table convertion_rate — aucun code applicatif ne s’en sert |
006-plans-max-cost-euro |
le plafond s’exprime en euros (renommage seul, le type reste entier) |
007-plans-max-cost-euro-numeric |
corrige le type du plafond ; ne restaure pas les décimales déjà arrondies |
008-provider-base-url-v1 |
ajoute /v1 aux URL de base d’Anthropic et OpenAI restées sans |
009-providers-slug-constraint-name |
renomme une contrainte d’unicité ; aucun effet fonctionnel |
010-retirer-les-cles-par-defaut |
retire le compte porteur de la clé publiée dans le dépôt |
011-plans-plafond-illimite |
un plan sans plafond devient illimité ; sème le plan administrator |
015-hacher-les-cles-api |
irréversible — remplace la clé en clair par son condensat, puis supprime la colonne |
La numérotation saute de 011 à 015, et c’est voulu. Les numéros 012 à
014 sont réservés à une famille de migrations qui n’est pas encore livrée. Le
script trie les noms de fichiers et n’exige aucune contiguïté : un trou n’est pas
une migration perdue, et il n’y a rien à aller chercher.
Deux détails qui peuvent dérouter à la lecture : l’en-tête du fichier 015 se
présente encore comme « Migration 012 », vestige d’une renumérotation ; et la
migration 010 porte un avertissement qui parle de « la 012 » — il désigne la même
015.
015 est à sens unique
Section intitulée « 015 est à sens unique »C’est la seule migration de la chaîne dont on ne revient pas, et elle mérite d’être comprise avant d’être lancée.
Avant elle, users.api_key contient la clé de chaque client en clair. La
migration calcule le condensat SHA-256 de chaque clé, en dérive le fragment
affichable (sk-lemniscate-…a3f9), puis supprime la colonne.
Après le DROP COLUMN, aucune requête, aucun écran, aucun accès à la base ne
retrouve une clé. La conversion elle-même est sans rupture pour les porteurs :
personne n’a à changer de clé, rien ne cesse de fonctionner. Ce qui disparaît, c’est
votre capacité à relire une clé — et donc à réparer une remise ratée autrement
qu’en faisant tourner la clé.
Une sauvegarde antérieure devient alors le seul endroit au monde où ces clés existent en clair. C’est la raison de la prendre, et la raison de la traiter ensuite comme un secret : la chiffrer, la sortir de la machine, la détruire une fois la migration confirmée. La garder « au cas où » rouvre exactement la fuite que la migration ferme.
Le tout tient dans une transaction unique, avec l’inscription au registre : la migration passe entièrement ou pas du tout.
Appliquer
Section intitulée « Appliquer »L’ordre compte. Le schéma et le code ne sont pas compatibles en avant : le code
d’avant lit api_key, qui n’existera plus, et le code neuf ne fonctionne pas sur
l’ancien schéma. Il y a donc nécessairement une fenêtre pendant laquelle
l’authentification est en panne, de la durée du redéploiement. Elle est assumée par
le produit ; prévoyez-la.
-
Sauvegarder, et traiter le fichier comme un secret.
Fenêtre de terminal ./sauvegarder.sh --sortie /var/sauvegardes/avant-migration-$(date +%Y%m%d-%H%M%S).sql.gzGardez l’inventaire que le script affiche : c’est ce que vous comparerez si vous devez revenir en arrière. Le détail est dans Sauvegarder et restaurer la base de la passerelle.
-
Relever ce qui va s’exécuter.
Fenêtre de terminal ./migrate.sh --status -
Compter les comptes avant, pour avoir un nombre à retrouver après.
Fenêtre de terminal psql "$DATABASE_URL" -c 'SELECT count(*) FROM users' -
Appliquer.
Fenêtre de terminal ./migrate.shChaque migration s’exécute dans une transaction unique avec son inscription au registre
schema_migrations: une migration qui échoue ne laisse ni schéma à moitié converti, ni registre menteur. -
Vérifier, si
015faisait partie du lot appliqué. Les trois nombres doivent être égaux entre eux et égaux à celui de l’étape 3.Fenêtre de terminal psql "$DATABASE_URL" -c "SELECT count(*) AS comptes, count(*) FILTER (WHERE api_key_hash ~ '^[0-9a-f]{64}$') AS haches, count(*) FILTER (WHERE api_key_hint <> '') AS avec_fragment FROM users"Et la colonne en clair doit avoir disparu — zéro ligne attendue :
Fenêtre de terminal psql "$DATABASE_URL" -c "SELECT column_name FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'api_key'" -
Redéployer la passerelle et la console, dans un ordre indifférent, mais après la migration.
-
Contrôler. Authentifier une clé connue contre la passerelle, et ouvrir la liste des comptes dans la console : chaque ligne doit porter son fragment.
Le cas d’une base qui n’a pas de registre
Section intitulée « Le cas d’une base qui n’a pas de registre »migrate.sh tient un registre (schema_migrations) qu’il crée au premier appel.
Une base dont le schéma a été monté autrement que par le script n’a pas ce registre,
et le script, ne trouvant rien d’inscrit, voudra tout rejouer depuis 000 — ce qui,
sur des données vivantes, les détruirait.
C’est à cela que sert le jalonnement, une seule fois :
./migrate.sh --baseline 006 # inscrit 000 à 006 sans les exécuter./migrate.sh # applique 007 et les suivantesAprès quoi ./migrate.sh seul suffit pour toujours.
La valeur 006 n’est pas générique. C’est celle de la base de production dont
l’histoire est connue : 001 à 006 y ont été passées à la main, sans registre. Le
dépôt ne documente aucune autre valeur, et --baseline inscrit sans exécuter :
un jalon posé trop loin marque comme appliquées des migrations qui ne l’ont jamais
été, et ces migrations-là ne s’exécuteront plus jamais. Sur 015, cela laisse des
clés en clair dans une base que tout le reste du système croit converti.
Ne posez un jalon que sur une base dont vous savez, par ailleurs, jusqu’où le schéma est déjà monté.
Cas particulier à reconnaître : une base créée par docker-compose a reçu
init.sql, qui décrit l’état cible complet du schéma — un test d’intégration
compare colonne par colonne les deux constructions. La chaîne ne s’y applique donc
pas telle quelle : 000 y recrée des tables qui existent déjà, et échoue. Le dépôt
ne documente pas de jalon pour ce cas.
Ajouter une migration
Section intitulée « Ajouter une migration »Si vous étendez le schéma vous-même, trois gestes, dans cet ordre :
- créer
migrations/0NN-description-courte.sql— le préfixe à trois chiffres fixe l’ordre d’exécution ; - répercuter le même effet dans
init.sql, qui décrit l’état cible complet ; - lancer la suite d’intégration : le test de schéma échoue si les deux descriptions ont divergé.
La migration 010 fait exception à toute retouche : son en-tête interdit
explicitement de la modifier, parce qu’elle s’exécute sur un schéma qui a encore
users.api_key. Une clé publiée après 015 se purge dans une migration neuve, sur
le condensat, pas en éditant 010.