use tokio_postgres::Client;
use pgevolve_core::plan::Plan;
use crate::executor::ApplyError;
use crate::target_identity::compute_cluster_target_identity;
#[derive(Debug, Clone, Default)]
pub struct ClusterPreflightOverrides {
pub allow_different_target: bool,
pub allow_unapproved_intents: bool,
}
pub async fn run_cluster_preflight(
client: &Client,
plan: &Plan,
overrides: ClusterPreflightOverrides,
) -> Result<(), ApplyError> {
if !overrides.allow_different_target {
let live = compute_cluster_target_identity(client).await?;
if live != plan.metadata.target_identity {
return Err(ApplyError::TargetIdentityMismatch {
plan: plan.metadata.target_identity.clone(),
live,
});
}
}
if !overrides.allow_unapproved_intents {
check_intent_approval(plan)?;
}
Ok(())
}
fn check_intent_approval(plan: &Plan) -> Result<(), ApplyError> {
let unapproved: Vec<_> = plan
.intents
.iter()
.filter(|i| !i.approved)
.map(|i| (i.id, i.target.clone(), i.reason.clone()))
.collect();
if !unapproved.is_empty() {
return Err(ApplyError::UnapprovedIntents {
count: unapproved.len(),
details: unapproved,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use pgevolve_core::ir::catalog::Catalog;
use pgevolve_core::plan::{DestructiveIntent, Plan, PlanId, PlanMetadata};
use time::OffsetDateTime;
fn plan_with_identity_and_intents(identity: &str, intents: Vec<DestructiveIntent>) -> Plan {
Plan {
id: PlanId([0u8; 32]),
groups: vec![],
intents,
lint_waivers: vec![],
step_overrides: vec![],
metadata: PlanMetadata {
pgevolve_version: "0.0.0-test".into(),
planner_ruleset_version: 0,
source_rev: None,
target_identity: identity.into(),
target_snapshot: Catalog::empty(),
created_at: OffsetDateTime::now_utc(),
lint_at_plan_findings: vec![],
},
advisory_findings: vec![],
}
}
#[test]
fn unapproved_intent_rejected() {
let intent = DestructiveIntent {
id: 1,
step: 1,
kind: "drop_role".into(),
target: "alice".into(),
reason: "drops role alice".into(),
approved: false,
};
let plan = plan_with_identity_and_intents("cluster:abc", vec![intent]);
let result = check_intent_approval(&plan);
assert!(
matches!(result, Err(ApplyError::UnapprovedIntents { count: 1, .. })),
"expected UnapprovedIntents(1), got: {result:?}"
);
}
#[test]
fn approved_intent_passes() {
let intent = DestructiveIntent {
id: 1,
step: 1,
kind: "drop_role".into(),
target: "alice".into(),
reason: "drops role alice".into(),
approved: true,
};
let plan = plan_with_identity_and_intents("cluster:abc", vec![intent]);
assert!(
check_intent_approval(&plan).is_ok(),
"expected Ok when all intents are approved"
);
}
#[test]
fn multiple_unapproved_intents_all_reported() {
let intents = vec![
DestructiveIntent {
id: 1,
step: 1,
kind: "drop_role".into(),
target: "alice".into(),
reason: "drops role alice".into(),
approved: false,
},
DestructiveIntent {
id: 2,
step: 2,
kind: "drop_role".into(),
target: "bob".into(),
reason: "drops role bob".into(),
approved: false,
},
];
let plan = plan_with_identity_and_intents("cluster:abc", intents);
let result = check_intent_approval(&plan);
assert!(
matches!(result, Err(ApplyError::UnapprovedIntents { count: 2, .. })),
"expected UnapprovedIntents(2), got: {result:?}"
);
}
#[test]
fn no_intents_passes() {
let plan = plan_with_identity_and_intents("cluster:abc", vec![]);
assert!(
check_intent_approval(&plan).is_ok(),
"expected Ok when there are no intents"
);
}
}