Version 1.0.0
Apply database migrations
Evolve the gateway schema without destroying keys, and the one migration you cannot undo.
This page describes how to bring the database of an already deployed gateway up to the current schema, and the migration that cannot be undone.
Two things to read before any migration
Section titled “Two things to read before any migration”The list of pending migrations
Section titled “The list of pending migrations”The list of migrations that will run is not written down in any document. The
script computes it from the files present in migrations/ and from what the database
has already received. A hand-maintained copy of that list goes stale; an
administrator who follows it may think they are applying a constraint rename
when the chain contains an irreversible migration.
export DATABASE_URL="postgresql://<utilisateur>:<mot-de-passe>@<hôte>:<port>/<base>"cd gateway-db./migrate.sh --statusEach [ ] line is a migration that will run on the next call without options.
Read them all before you launch anything.
A migration is applied on top of a backup you know how to restore
Section titled “A migration is applied on top of a backup you know how to restore”Taking a backup is not enough. What protects you is having already restored a backup at least once, cold, on this installation. Until you have done the round trip, you have files that nobody knows will come back.
The procedure (the two scripts, the inventory to compare before and after, the restore test on a side database) is in Back up and restore the gateway database. Do it before coming back here.
Two points it covers that bear on what follows:
- the backup does not carry the secrets that live outside the database: not
your providers’ keys, of which the database only knows the environment
variable name, and not
ADMIN_API_KEY. A restored database does not make an installation that starts again; - the file is a secret: before the
015migration, it contains every one of your holders’ keys in clear text.
The snapshots managed by your host remain useful, but they do not replace this test: neither their frequency, nor their retention, nor their exact contents can be verified from here.
The migrations that exist today
Section titled “The migrations that exist today”They are listed, in the order they apply, by the migration reference. That page is generated from the files themselves: it cannot omit a migration that exists, nor describe one that does not. This guide keeps no copy of it.
The script sorts file names and requires no contiguity: a gap in the numbering is not a lost migration.
015 is one-way
Section titled “015 is one-way”It is the only migration in the chain that cannot be undone.
Before it, users.api_key contains each client’s key in clear text. The migration
computes the SHA-256 digest of each key, derives the displayable fragment from
it (sk-lemniscate-…a3f9), then drops the column.
After the DROP COLUMN, no query, no screen, no database access retrieves a key. The
conversion is seamless for holders: nobody has to change their key. What you
lose is the ability to read a key back, and therefore to fix a failed handover
other than by rotating the key.
An earlier backup then becomes the only place where those keys exist in clear text. Treat it as a secret: encrypt it, move it off the machine, destroy it once the migration is confirmed. Keeping it “just in case” reopens the leak the migration closes.
The whole thing fits in a single transaction, along with the registry entry: the migration goes through entirely or not at all.
Applying
Section titled “Applying”Order matters. Schema and code are not forward compatible: the old code reads
api_key, which no longer exists, and the new code does not work on the old
schema. There is therefore a window during which authentication is down, for
the duration of the redeployment. Plan for it.
-
Back up, and treat the file as a secret.
Fenêtre de terminal ./sauvegarder.sh --sortie /var/sauvegardes/avant-migration-$(date +%Y%m%d-%H%M%S).sql.gzKeep the inventory the script prints: that is what you will compare against if you have to roll back. The details are in Back up and restore the gateway database.
-
Note what is going to run.
Fenêtre de terminal ./migrate.sh --status -
Count the accounts beforehand, so you have a number to find again after.
Fenêtre de terminal psql "$DATABASE_URL" -c 'SELECT count(*) FROM users' -
Apply.
Fenêtre de terminal ./migrate.shEach migration runs in a single transaction along with its entry in the
schema_migrationsregistry: a migration that fails leaves neither a half-converted schema nor a wrong registry. -
Verify, if
015or021was part of the batch applied. The three numbers must be equal to each other and equal to the one from step 3.Fenêtre de terminal psql "$DATABASE_URL" -c "SELECT count(*) AS cles, count(*) FILTER (WHERE key_hash ~ '^[0-9a-f]{64}$') AS hachees, count(*) FILTER (WHERE key_hint <> '') AS avec_fragment FROM access_keys"No key column must remain on the accounts; zero rows is expected:
Fenêtre de terminal psql "$DATABASE_URL" -c "SELECT column_name FROM information_schema.columns WHERE table_name = 'users' AND column_name LIKE '%key%'" -
Put the service roles into service, if
023was part of the batch applied.This migration creates two PostgreSQL roles,
lemniscate_gatewayandlemniscate_console, each with only the rights its service needs. It creates them without a password and without the right to connect (NOLOGIN): a secret written into the product would be a secret published to everyone who installs it. Open them with your own secrets:Fenêtre de terminal psql "$DATABASE_URL" -c "ALTER ROLE lemniscate_gateway WITH LOGIN PASSWORD 'votre-secret'"psql "$DATABASE_URL" -c "ALTER ROLE lemniscate_console WITH LOGIN PASSWORD 'un-autre'"Then, in each service’s configuration, replace the owner account identifier with that of its role. The gateway takes
lemniscate_gateway, the consolelemniscate_console. -
Redeploy the gateway and the console, in either order relative to each other, but after the migration.
-
Check. Authenticate a known key against the gateway, and open the account list in the console: every row must carry its fragment.
The case of a database with no registry
Section titled “The case of a database with no registry”migrate.sh keeps a registry (schema_migrations) that it creates on the first call. A database
whose schema was built by something other than the script does not have that
registry. Finding nothing recorded, the script would want to replay everything
from 000, which would destroy live data.
Stamping is for that case, once only:
./migrate.sh --baseline 006 # inscrit 000 à 006 sans les exécuter./migrate.sh # applique 007 et les suivantesAfter that, ./migrate.sh without options applies the following migrations.
The value 006 is not generic. It is the value for a database whose history is
known: 001 through 006 were applied by hand, with no registry. --baseline
records without running: a stamp placed too far ahead marks as applied
migrations that were not, and those migrations never run. On 015, that leaves
keys in clear text in a database the rest of the system believes converted.
Only place a stamp on a database where you know, by other means, how far the schema was built.
The second case is a database created by docker-compose, which received init.sql. That file
describes the complete target state of the schema, and an integration test
compares the two constructions column by column. The chain does not apply to it
as is: 000 recreates tables that already exist there, and fails. The stamp to
place is that of the last migration in the repository;
the migration reference gives it, and the tutorial
Get started with the gateway prescribes it too.
Adding a migration
Section titled “Adding a migration”If you extend the schema yourself, three steps, in this order:
- create
migrations/0NN-description-courte.sql; the three-digit prefix fixes the execution order; - mirror the same effect in
init.sql, which describes the complete target state; - run the integration suite: the schema test fails if the two descriptions have diverged.
The 010 migration is not to be edited: its header forbids it, because it runs
on a schema that still has users.api_key. A key published after 015 is purged in a new
migration, on the digest.