mix encryptor.ecto.gen.key_store_shape_migration (Encryptor.Ecto v0.4.0)

Copy Markdown View Source

Generates the migration that brings an existing wrapped-key table up to ADR-0005's row shape.

mix encryptor.ecto.gen.key_store_shape_migration [--table NAME] [--migrations-path PATH]

A table created under 0.3.0 holds the six fields Encryptor.Envelope.WrappedKey fixes and nothing else. Encryptor.Ecto.KeyStore now also selects wrapping_shape and key_id (ADR-0005 decisions 1 and 2), so it names two columns that table does not have. Run this migration before deploying the new version, not after: until it runs, every read of that table raises the Postgrex.Error naming the column that is missing, which is a permanent condition and reports itself as one (ADR-0005 decision 7, and its open question 3, now answered - the bare rescue that used to report this as a retryable {:key_unavailable, selector} is gone).

Why this is a second task

mix encryptor.ecto.gen.key_store_migration writes CREATE TABLE and refuses, with exit 2, where a migration for the table already exists - a repeated CREATE TABLE fails on the way up. That refusal is right and it is also why that task cannot serve an adopter: an existing table needs an ALTER, not a second CREATE. Fresh adopters need only the first task, whose DDL already carries both columns.

This task issues no DDL

Like its sibling it writes one file, opens no database connection, and runs nothing (ADR-0002 decision 9). The file is yours the moment it lands: review it in a diff, commit it, and run it with your own mix ecto.migrate on your own deploy schedule. The module name is generated from this project's application name and will need renaming if your repo lives in another namespace.

Flags

Flag
--table NAMEencryptor_wrapped_keysThe table to alter. A host that renamed it passes the same name it passes as table: in its Encryptor.Ecto.KeyStore provider options
--migrations-path PATHpriv/repo/migrationsWhere to write the file

Exit codes

0The file was written; its path is printed
2Usage error, or an additive migration for this table already exists in that directory - the generator never overwrites one and never writes a second

The shape it writes

Two alter blocks, in that order, and no index.

The first adds wrapping_shape with default: "engine_message", which is what makes null: false possible on a populated table in one statement. The default is correct rather than convenient: the shipped 0.3.0 read path calls Encryptor.Envelope.unwrap/2 for every row with no branch at all, so a row that is not an engine message is a row that code could never have read. There are none.

The second sets that default to NULL, in the same migration. A permanent default would mean a host inserting a GCP-wrapped row and forgetting the column gets a row that claims to be an engine message and fails later, during someone else's rotation. from: on the modify is what makes the whole thing reversible, so mix ecto.rollback is available on the way back.

No index: neither column is ever a lookup key - the lookup key is tenant_ref - and an index on a two-valued column over a table with one row per tenant per version buys nothing.