Centralize durable schema migration ownership
This commit is contained in:
parent
cc328237f4
commit
069f3813ae
9 changed files with 354 additions and 350 deletions
|
|
@ -22,4 +22,5 @@ time.workspace = true
|
|||
tokio.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
remora-schema.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "time"] }
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
-- Source credential registry metadata. Secret material lives in Vault KV v2;
|
||||
-- this package only stores owner, kind, status, and opaque version coordinates.
|
||||
|
||||
CREATE TABLE source_credentials (
|
||||
credential_id TEXT PRIMARY KEY,
|
||||
owner_id TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
active_version_id TEXT,
|
||||
secret_mount TEXT NOT NULL,
|
||||
secret_path TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT source_credentials_kind_check CHECK (
|
||||
kind IN (
|
||||
'basic_auth',
|
||||
'bearer_token',
|
||||
'http_headers',
|
||||
'object_store',
|
||||
'hugging_face'
|
||||
)
|
||||
),
|
||||
CONSTRAINT source_credentials_status_check CHECK (status IN ('active', 'revoked')),
|
||||
CONSTRAINT source_credentials_label_len CHECK (
|
||||
char_length(label) BETWEEN 1 AND 200
|
||||
),
|
||||
CONSTRAINT source_credentials_active_status CHECK (
|
||||
(status = 'active' AND active_version_id IS NOT NULL)
|
||||
OR (status = 'revoked')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX source_credentials_owner_created_idx
|
||||
ON source_credentials (owner_id, created_at DESC, credential_id DESC);
|
||||
|
||||
CREATE TABLE source_credential_versions (
|
||||
version_id TEXT PRIMARY KEY,
|
||||
credential_id TEXT NOT NULL REFERENCES source_credentials (credential_id),
|
||||
vault_kv_version BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
destroyed_at TIMESTAMPTZ,
|
||||
CONSTRAINT source_credential_versions_kv_positive CHECK (vault_kv_version >= 1)
|
||||
);
|
||||
|
||||
CREATE INDEX source_credential_versions_credential_idx
|
||||
ON source_credential_versions (credential_id, created_at DESC);
|
||||
|
||||
ALTER TABLE source_credentials
|
||||
ADD CONSTRAINT source_credentials_active_version_fk
|
||||
FOREIGN KEY (active_version_id)
|
||||
REFERENCES source_credential_versions (version_id)
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
-- Durable intent for Vault secret writes. Secret material is never stored here.
|
||||
-- Intents make create/rotate recoverable across ambiguous PostgreSQL commits
|
||||
-- without best-effort Vault destruction that could erase a committed version.
|
||||
|
||||
CREATE TABLE secret_write_intents (
|
||||
intent_id TEXT PRIMARY KEY,
|
||||
operation TEXT NOT NULL,
|
||||
credential_id TEXT NOT NULL,
|
||||
version_id TEXT NOT NULL,
|
||||
owner_id TEXT,
|
||||
label TEXT,
|
||||
credential_kind TEXT,
|
||||
secret_mount TEXT NOT NULL,
|
||||
secret_path TEXT NOT NULL,
|
||||
vault_kv_version BIGINT,
|
||||
status TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT secret_write_intents_operation_check CHECK (
|
||||
operation IN ('create', 'rotate')
|
||||
),
|
||||
CONSTRAINT secret_write_intents_status_check CHECK (
|
||||
status IN ('prepared', 'vault_written', 'durable_committed', 'abandoned')
|
||||
),
|
||||
CONSTRAINT secret_write_intents_kv_check CHECK (
|
||||
vault_kv_version IS NULL OR vault_kv_version >= 1
|
||||
),
|
||||
CONSTRAINT secret_write_intents_create_fields_check CHECK (
|
||||
(operation = 'create'
|
||||
AND owner_id IS NOT NULL
|
||||
AND label IS NOT NULL
|
||||
AND credential_kind IS NOT NULL)
|
||||
OR (operation = 'rotate'
|
||||
AND owner_id IS NULL
|
||||
AND label IS NULL
|
||||
AND credential_kind IS NULL)
|
||||
),
|
||||
CONSTRAINT secret_write_intents_written_has_version CHECK (
|
||||
(status = 'prepared' AND vault_kv_version IS NULL)
|
||||
OR (status IN ('vault_written', 'durable_committed')
|
||||
AND vault_kv_version IS NOT NULL)
|
||||
OR status = 'abandoned'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX secret_write_intents_status_idx
|
||||
ON secret_write_intents (status, created_at);
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
-- Durable execution-grant credential pins. Resolution always re-reads this table:
|
||||
-- revoked or missing grants fail closed even if a worker still holds a grant id.
|
||||
|
||||
CREATE TABLE execution_grant_credential_pins (
|
||||
grant_id TEXT PRIMARY KEY,
|
||||
credential_id TEXT NOT NULL,
|
||||
version_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
CONSTRAINT execution_grant_credential_pins_status_check CHECK (
|
||||
status IN ('active', 'revoked')
|
||||
),
|
||||
CONSTRAINT execution_grant_credential_pins_revoked_at_check CHECK (
|
||||
(status = 'active' AND revoked_at IS NULL)
|
||||
OR (status = 'revoked' AND revoked_at IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX execution_grant_credential_pins_credential_idx
|
||||
ON execution_grant_credential_pins (credential_id, status);
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
-- Reconciler ownership claim: vault destruction runs only after an intent is
|
||||
-- moved to abandoning under the credential lock. Create/rotate materialize
|
||||
-- refuses non-vault_written intents, so a concurrent commit cannot land a
|
||||
-- durable version that points at material the reconciler is about to destroy.
|
||||
|
||||
ALTER TABLE secret_write_intents
|
||||
DROP CONSTRAINT secret_write_intents_status_check;
|
||||
|
||||
ALTER TABLE secret_write_intents
|
||||
ADD CONSTRAINT secret_write_intents_status_check CHECK (
|
||||
status IN (
|
||||
'prepared',
|
||||
'vault_written',
|
||||
'durable_committed',
|
||||
'abandoning',
|
||||
'abandoned'
|
||||
)
|
||||
);
|
||||
|
||||
ALTER TABLE secret_write_intents
|
||||
DROP CONSTRAINT secret_write_intents_written_has_version;
|
||||
|
||||
ALTER TABLE secret_write_intents
|
||||
ADD CONSTRAINT secret_write_intents_written_has_version CHECK (
|
||||
(status = 'prepared' AND vault_kv_version IS NULL)
|
||||
OR (status IN ('vault_written', 'durable_committed', 'abandoning')
|
||||
AND vault_kv_version IS NOT NULL)
|
||||
OR status = 'abandoned'
|
||||
);
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
ALTER TABLE source_credential_versions
|
||||
ADD COLUMN secret_mount TEXT,
|
||||
ADD COLUMN secret_path TEXT;
|
||||
|
||||
UPDATE source_credential_versions AS version
|
||||
SET secret_mount = credential.secret_mount,
|
||||
secret_path = credential.secret_path
|
||||
FROM source_credentials AS credential
|
||||
WHERE credential.credential_id = version.credential_id;
|
||||
|
||||
ALTER TABLE source_credential_versions
|
||||
ALTER COLUMN secret_mount SET NOT NULL,
|
||||
ALTER COLUMN secret_path SET NOT NULL;
|
||||
|
|
@ -9,7 +9,6 @@
|
|||
mod authz;
|
||||
mod error;
|
||||
mod ids;
|
||||
mod migrate;
|
||||
mod registry;
|
||||
mod resolve;
|
||||
mod types;
|
||||
|
|
@ -17,7 +16,6 @@ mod vault;
|
|||
|
||||
pub use authz::{CredentialCaller, may_manage, require_manage};
|
||||
pub use error::CredentialError;
|
||||
pub use migrate::{migrate, migrator};
|
||||
pub use registry::{
|
||||
CredentialRegistry, PreparedCredentialCreate, PreparedCredentialRotate, RotateTxOutcome,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
use remora_durable::DurableError;
|
||||
use sqlx::PgPool;
|
||||
|
||||
/// Separate migration history from the DurableStore substrate so feature-package
|
||||
/// versions do not collide with `remora-durable` in `_sqlx_migrations`.
|
||||
const MIGRATION_TABLE: &str = "_remora_credentials_migrations";
|
||||
|
||||
pub async fn migrate(pool: &PgPool) -> Result<(), DurableError> {
|
||||
let mut migrator = sqlx::migrate!("./migrations");
|
||||
migrator.dangerous_set_table_name(MIGRATION_TABLE);
|
||||
migrator.run(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn migrator() -> sqlx::migrate::Migrator {
|
||||
let mut migrator = sqlx::migrate!("./migrations");
|
||||
migrator.dangerous_set_table_name(MIGRATION_TABLE);
|
||||
migrator
|
||||
}
|
||||
|
|
@ -4,11 +4,12 @@ use std::time::Duration;
|
|||
|
||||
use remora_credentials::{
|
||||
ActiveCredentialGrant, CreateCredential, CredentialCaller, CredentialGrantVersion,
|
||||
CredentialRegistry, GrantBoundResolver, SecretMaterial, VaultClient, VaultConfig, migrate,
|
||||
CredentialRegistry, GrantBoundResolver, SecretMaterial, VaultClient, VaultConfig,
|
||||
};
|
||||
use remora_domain::OpaqueId;
|
||||
use remora_domain::ports::CredentialResolver;
|
||||
use remora_durable::{PoolConfig, connect, migrate as durable_migrate};
|
||||
use remora_durable::{PoolConfig, connect};
|
||||
use remora_schema::migrate as migrate_schema;
|
||||
use sqlx::PgPool;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
|
@ -57,8 +58,7 @@ async fn test_pool() -> Option<PgPool> {
|
|||
)
|
||||
.await
|
||||
.expect("connect postgres");
|
||||
durable_migrate(&pool).await.expect("durable migrate");
|
||||
migrate(&pool).await.expect("credentials migrate");
|
||||
migrate_schema(&pool).await.expect("schema migrate");
|
||||
Some(pool)
|
||||
}
|
||||
|
||||
|
|
@ -1218,8 +1218,8 @@ async fn migration_is_idempotent() {
|
|||
let Some(pool) = test_pool().await else {
|
||||
return;
|
||||
};
|
||||
migrate(&pool).await.expect("second");
|
||||
migrate(&pool).await.expect("third");
|
||||
migrate_schema(&pool).await.expect("second");
|
||||
migrate_schema(&pool).await.expect("third");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -1245,10 +1245,12 @@ async fn version_coordinate_migration_backfills_populated_schema() {
|
|||
.execute(&mut *connection)
|
||||
.await
|
||||
.expect("select migration schema");
|
||||
sqlx::raw_sql(include_str!("../migrations/0001_credentials.sql"))
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.expect("pre-coordinate schema");
|
||||
sqlx::raw_sql(include_str!(
|
||||
"../../remora-schema/migrations/credentials/0001_credentials.sql"
|
||||
))
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.expect("pre-coordinate schema");
|
||||
sqlx::query(
|
||||
"INSERT INTO source_credentials (
|
||||
credential_id, owner_id, label, kind, status, active_version_id,
|
||||
|
|
@ -1274,7 +1276,7 @@ async fn version_coordinate_migration_backfills_populated_schema() {
|
|||
.await
|
||||
.expect("legacy version");
|
||||
sqlx::raw_sql(include_str!(
|
||||
"../migrations/0005_version_secret_coordinates.sql"
|
||||
"../../remora-schema/migrations/credentials/0005_version_secret_coordinates.sql"
|
||||
))
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
|
|
|
|||
Loading…
Reference in a new issue