feat: harden community production foundation through story 1.5
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
use std::{process::ExitCode, time::Duration};
|
||||
|
||||
use crank_config::{ConfigSource, DatabaseSettings, parse_migrator};
|
||||
use crank_registry::{
|
||||
BackfillPolicy, MigrationApplyResult, MigrationAuthority, MigrationPreflight,
|
||||
};
|
||||
use serde_json::json;
|
||||
use sqlx::{
|
||||
PgPool,
|
||||
postgres::{PgConnectOptions, PgPoolOptions},
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
match run().await {
|
||||
Ok(code) => code,
|
||||
Err(error) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
json!({
|
||||
"status": "error",
|
||||
"code": error.code,
|
||||
"stage": error.stage,
|
||||
"version": error.version,
|
||||
"recovery": error.recovery,
|
||||
})
|
||||
);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct CliError {
|
||||
code: &'static str,
|
||||
stage: &'static str,
|
||||
recovery: &'static str,
|
||||
version: Option<i64>,
|
||||
}
|
||||
|
||||
impl CliError {
|
||||
const fn new(code: &'static str, stage: &'static str, recovery: &'static str) -> Self {
|
||||
Self {
|
||||
code,
|
||||
stage,
|
||||
recovery,
|
||||
version: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_migration(error: crank_registry::MigrationError) -> Self {
|
||||
Self {
|
||||
code: error.code(),
|
||||
stage: error.stage(),
|
||||
recovery: error.recovery(),
|
||||
version: error.version(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run() -> Result<ExitCode, CliError> {
|
||||
let mut arguments = std::env::args().skip(1);
|
||||
let requested = arguments.next();
|
||||
let option = arguments.next();
|
||||
if arguments.next().is_some() {
|
||||
return Err(CliError::new(
|
||||
"invalid_command",
|
||||
"cli.arguments",
|
||||
"run_preflight",
|
||||
));
|
||||
}
|
||||
let command = match requested.as_deref() {
|
||||
None | Some("preflight") => "preflight",
|
||||
Some("plan") => "plan",
|
||||
Some("apply") => "apply",
|
||||
Some(_) => {
|
||||
return Err(CliError::new(
|
||||
"invalid_command",
|
||||
"cli.arguments",
|
||||
"run_preflight",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if command == "plan" {
|
||||
MigrationAuthority::validate_sequence().map_err(CliError::from_migration)?;
|
||||
let sequence = MigrationAuthority::sequence()
|
||||
.into_iter()
|
||||
.map(|migration| {
|
||||
let backfill = match migration.backfill {
|
||||
BackfillPolicy::None => json!({ "kind": "none" }),
|
||||
BackfillPolicy::Bounded {
|
||||
max_batch_rows,
|
||||
max_batch_ms,
|
||||
resumable,
|
||||
} => json!({
|
||||
"kind": "bounded",
|
||||
"max_batch_rows": max_batch_rows,
|
||||
"max_batch_ms": max_batch_ms,
|
||||
"resumable": resumable,
|
||||
}),
|
||||
};
|
||||
json!({
|
||||
"version": migration.version,
|
||||
"name": migration.name,
|
||||
"checksum": migration.checksum,
|
||||
"source_digest": migration.source_digest,
|
||||
"phase": migration.phase,
|
||||
"compatibility": migration.compatibility,
|
||||
"owner": migration.owner,
|
||||
"transactional": migration.transactional,
|
||||
"backfill": backfill,
|
||||
"readable_schema_min": migration.readable_schema_min,
|
||||
"readable_schema_max": migration.readable_schema_max,
|
||||
"contract_evidence": migration.contract_evidence,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let plan = json!({ "schema_version": 1, "sequence": sequence });
|
||||
if option.as_deref() == Some("--check") {
|
||||
let bytes = std::fs::read("docs/schemas/migration-sequence.json")
|
||||
.map_err(|_| CliError::new("contract_drift", "plan.read", "contact_operator"))?;
|
||||
if bytes.len() > 65_536 {
|
||||
return Err(CliError::new(
|
||||
"contract_drift",
|
||||
"plan.size",
|
||||
"contact_operator",
|
||||
));
|
||||
}
|
||||
let committed: serde_json::Value = serde_json::from_slice(&bytes)
|
||||
.map_err(|_| CliError::new("contract_drift", "plan.parse", "contact_operator"))?;
|
||||
if committed != plan {
|
||||
return Err(CliError::new(
|
||||
"contract_drift",
|
||||
"plan.compare",
|
||||
"contact_operator",
|
||||
));
|
||||
}
|
||||
println!("{}", json!({ "status": "contract_current" }));
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
if option.is_some() {
|
||||
return Err(CliError::new(
|
||||
"invalid_command",
|
||||
"cli.arguments",
|
||||
"run_preflight",
|
||||
));
|
||||
}
|
||||
println!("{plan}");
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
if option.is_some() {
|
||||
return Err(CliError::new(
|
||||
"invalid_command",
|
||||
"cli.arguments",
|
||||
"run_preflight",
|
||||
));
|
||||
}
|
||||
|
||||
let config = parse_migrator(
|
||||
ConfigSource::from_os_for_migrator()
|
||||
.map_err(|_| CliError::new("config_invalid", "config.source", "run_preflight"))?,
|
||||
)
|
||||
.map_err(|_| CliError::new("config_invalid", "config.validate", "run_preflight"))?;
|
||||
let pool = connect(&config.database).await?;
|
||||
|
||||
if command == "apply" {
|
||||
let result = MigrationAuthority::apply(&pool)
|
||||
.await
|
||||
.map_err(CliError::from_migration)?;
|
||||
let (status, from, to) = match result {
|
||||
MigrationApplyResult::Applied { from, to } => ("applied", from, to),
|
||||
MigrationApplyResult::AlreadyCurrent { version } => {
|
||||
("already_current", version, version)
|
||||
}
|
||||
};
|
||||
println!(
|
||||
"{}",
|
||||
json!({ "status": status, "from_version": from, "to_version": to })
|
||||
);
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
match MigrationAuthority::preflight(&pool)
|
||||
.await
|
||||
.map_err(CliError::from_migration)?
|
||||
{
|
||||
MigrationPreflight::Current { version } => {
|
||||
println!("{}", json!({ "status": "current", "version": version }));
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
MigrationPreflight::MigrationRequired { current, target } => {
|
||||
println!(
|
||||
"{}",
|
||||
json!({
|
||||
"status": "migration_required",
|
||||
"current_version": current,
|
||||
"target_version": target,
|
||||
"recovery": "run_controlled_migration",
|
||||
})
|
||||
);
|
||||
Ok(ExitCode::from(2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect(config: &DatabaseSettings) -> Result<PgPool, CliError> {
|
||||
let options = if let Some(url) = &config.url {
|
||||
url.expose_secret()
|
||||
.parse::<PgConnectOptions>()
|
||||
.map_err(|_| CliError::new("config_invalid", "database.source", "run_preflight"))?
|
||||
} else {
|
||||
PgConnectOptions::new()
|
||||
.host(&config.host)
|
||||
.port(config.port)
|
||||
.database(&config.database)
|
||||
.username(&config.username)
|
||||
.password(config.password.expose_secret())
|
||||
};
|
||||
for attempt in 1..=10 {
|
||||
let result = PgPoolOptions::new()
|
||||
.max_connections(config.pool.max_connections)
|
||||
.min_connections(config.pool.min_connections)
|
||||
.acquire_timeout(Duration::from_millis(config.pool.acquire_timeout_ms))
|
||||
.idle_timeout(Duration::from_millis(config.pool.idle_timeout_ms))
|
||||
.max_lifetime(Duration::from_millis(config.pool.max_lifetime_ms))
|
||||
.connect_with(options.clone())
|
||||
.await;
|
||||
match result {
|
||||
Ok(pool) => return Ok(pool),
|
||||
Err(_) if attempt < 10 => {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
Err(CliError::new(
|
||||
"storage_unavailable",
|
||||
"database.connect",
|
||||
"contact_operator",
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user