pub mod audit;
pub mod bootstrap;
pub mod cluster_apply;
pub mod cluster_preflight;
pub mod env_interp;
pub mod error;
pub mod execute;
pub mod lock;
pub mod preflight;
pub mod status;
pub use cluster_apply::{ClusterApplyError, apply_cluster_plan, apply_cluster_plan_dir};
pub use cluster_preflight::ClusterPreflightOverrides;
use std::path::Path;
use tokio_postgres::Client;
use uuid::Uuid;
use pgevolve_core::catalog::CatalogFilter;
use pgevolve_core::plan::Plan;
pub use bootstrap::bootstrap_metadata;
pub use error::ApplyError;
pub use lock::{release_lock, try_acquire_lock};
pub use preflight::{PreflightOverrides, run_preflight};
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Default)]
pub struct ApplyOverrides {
pub allow_different_target: bool,
pub allow_drift: bool,
pub allow_unwaived_lint: bool,
pub allow_unapproved_intents: bool,
pub actor: Option<String>,
pub abort_after_step: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApplyOutcome {
Succeeded {
apply_id: Uuid,
},
}
pub async fn apply(
plan_dir: &Path,
client: &mut Client,
filter: &CatalogFilter,
overrides: ApplyOverrides,
) -> Result<ApplyOutcome, ApplyError> {
let plan = pgevolve_core::plan::read_plan_dir(plan_dir)?;
apply_plan(&plan, client, filter, overrides).await
}
pub async fn apply_plan(
plan: &Plan,
client: &mut Client,
filter: &CatalogFilter,
overrides: ApplyOverrides,
) -> Result<ApplyOutcome, ApplyError> {
bootstrap_metadata(client).await?;
let actor = overrides.actor.clone().unwrap_or_else(default_actor);
try_acquire_lock(client, &actor).await?;
let preflight = PreflightOverrides {
allow_different_target: overrides.allow_different_target,
allow_drift: overrides.allow_drift,
allow_unwaived_lint: overrides.allow_unwaived_lint,
allow_unapproved_intents: overrides.allow_unapproved_intents,
};
let preflight_result = run_preflight(client, plan, filter, preflight).await;
if let Err(e) = preflight_result {
let _ = release_lock(client).await;
return Err(e);
}
let apply_id = audit::open_apply_log(client, plan, &actor).await?;
let exec_result =
execute::execute_plan(client, plan, apply_id, overrides.abort_after_step).await;
match exec_result {
Ok(()) => {
audit::close_apply_log(client, apply_id, "succeeded", None).await?;
release_lock(client).await?;
Ok(ApplyOutcome::Succeeded { apply_id })
}
Err(ApplyError::AbortedAfterStep { step_no }) => {
audit::close_apply_log(
client,
apply_id,
"aborted",
Some(&format!("abort_after_step={step_no}")),
)
.await?;
let _ = release_lock(client).await;
Err(ApplyError::AbortedAfterStep { step_no })
}
Err(e) => {
let msg = e.to_string();
audit::close_apply_log(client, apply_id, "failed", Some(&msg)).await?;
let _ = release_lock(client).await;
Err(e)
}
}
}
pub(crate) fn default_actor() -> String {
let user = std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "unknown".into());
let host = hostname_string();
format!("{user}@{host}")
}
fn hostname_string() -> String {
std::fs::read_to_string("/etc/hostname")
.map(|s| s.trim().to_string())
.ok()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("HOSTNAME").ok())
.unwrap_or_else(|| "unknown".into())
}