Skip to content

migrations

migrations

Credential-DB schema bootstrap + forward migrations.

Two functions, both idempotent, both called by every opener of the sqlite3 file (CredentialDB for writers, the vault daemon's read-only _TokenDB for readers):

  • ensure_credentials_schema declares the current shape via CREATE TABLE IF NOT EXISTS so fresh installs land at the latest schema in one shot.
  • migrate_credential_db_schema walks legacy DBs forward step by step, gated by PRAGMA user_version so already-upgraded files are a no-op.

Splitting these out of db.py keeps the data-access layer free of ALTER TABLE machinery and gives schema changes a focused review target — every future bump touches one file.

SCHEMA_VERSION = 2 module-attribute

ensure_credentials_schema(conn)

Create the credential / SSH-key / phantom-token tables if missing.

Idempotent — every statement is IF NOT EXISTS. Exposed at module level so every opener of the DB file runs it before issuing queries. Without this, a daemon that opens an empty DB on a fresh install (before any CLI command has touched the file) hits no such table: credentials on the first query and crashes the unit.

Source code in src/terok_sandbox/vault/store/migrations.py
def ensure_credentials_schema(conn: sqlite3.Connection) -> None:
    """Create the credential / SSH-key / phantom-token tables if missing.

    Idempotent — every statement is ``IF NOT EXISTS``.  Exposed at module
    level so every opener of the DB file runs it before issuing queries.
    Without this, a daemon that opens an empty DB on a fresh install
    (before any CLI command has touched the file) hits ``no such table:
    credentials`` on the first query and crashes the unit.
    """
    conn.executescript("""
        CREATE TABLE IF NOT EXISTS credentials (
            credential_set TEXT NOT NULL,
            provider       TEXT NOT NULL,
            data           TEXT NOT NULL,
            PRIMARY KEY (credential_set, provider)
        );
        CREATE TABLE IF NOT EXISTS ssh_keys (
            id           INTEGER PRIMARY KEY AUTOINCREMENT,
            key_type     TEXT    NOT NULL CHECK (key_type IN ('ed25519','rsa')),
            private_der  BLOB    NOT NULL,
            public_blob  BLOB    NOT NULL,
            comment      TEXT    NOT NULL DEFAULT '',
            fingerprint  TEXT    NOT NULL UNIQUE,
            created_at   TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS ssh_key_assignments (
            scope        TEXT    NOT NULL,
            key_id       INTEGER NOT NULL REFERENCES ssh_keys(id) ON DELETE CASCADE,
            assigned_at  TEXT    NOT NULL DEFAULT (datetime('now')),
            PRIMARY KEY (scope, key_id)
        );
        CREATE TABLE IF NOT EXISTS proxy_tokens (
            token          TEXT PRIMARY KEY,
            scope          TEXT NOT NULL,
            subject        TEXT NOT NULL,
            credential_set TEXT NOT NULL,
            provider       TEXT NOT NULL
        );
    """)
    conn.commit()

migrate_credential_db_schema(conn)

Walk legacy credential-DB rows forward to the current schema.

Tracked via PRAGMA user_version so the whole function is a no-op on already-upgraded DBs. Each current < N branch handles one forward step; the final PRAGMA user_version set commits the whole upgrade in one go.

Exposed at module level so every opener of the DB file (CredentialDB for writers, _TokenDB in the vault daemon for readers) runs it before issuing queries — otherwise a daemon that restarts before any CLI command has touched the DB would hit "no such column: …" on a freshly-upgraded host.

The cryptography import is scoped to the v0 → v1 branch so already-migrated DBs (the common case) don't pay an import cost, and the storage module keeps tach-clean at import time.

Source code in src/terok_sandbox/vault/store/migrations.py
def migrate_credential_db_schema(conn: sqlite3.Connection) -> None:
    """Walk legacy credential-DB rows forward to the current schema.

    Tracked via ``PRAGMA user_version`` so the whole function is a no-op
    on already-upgraded DBs.  Each ``current < N`` branch handles one
    forward step; the final ``PRAGMA user_version`` set commits the
    whole upgrade in one go.

    Exposed at module level so every opener of the DB file
    ([`CredentialDB`][terok_sandbox.vault.store.db.CredentialDB] for
    writers, ``_TokenDB`` in the vault daemon for readers) runs it
    before issuing queries — otherwise a daemon that restarts before any
    CLI command has touched the DB would hit "no such column: …" on a
    freshly-upgraded host.

    The ``cryptography`` import is scoped to the v0 → v1 branch so
    already-migrated DBs (the common case) don't pay an import cost,
    and the storage module keeps tach-clean at import time.
    """
    (current,) = conn.execute("PRAGMA user_version").fetchone()
    if current >= SCHEMA_VERSION:
        return

    if current < 1:
        _migrate_v0_to_v1(conn)

    if current < 2:
        _migrate_v1_to_v2(conn)

    conn.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
    conn.commit()