use crate::{
diag::{Diagnostic, SgCode},
selector::{ProjectMismatch, Target, resolve_target},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PurgePlan {
Everything,
Config {
projects: Vec<String>,
},
Project {
project: String,
},
}
pub fn resolve_plan(
service: Option<&str>,
project: Option<&str>,
config_projects: Option<Vec<String>>,
) -> Result<PurgePlan, ProjectMismatch> {
Ok(match resolve_target(service, project)? {
Target::Everything => match config_projects {
Some(projects) => PurgePlan::Config { projects },
None => PurgePlan::Everything,
},
Target::Project { project } => PurgePlan::Project { project },
Target::Service { service, project } => PurgePlan::Project {
project: project.unwrap_or(service),
},
})
}
#[derive(Debug, Clone, Copy)]
pub struct World {
pub supervisor_serving: bool,
pub managed_units: usize,
pub force: bool,
}
#[derive(Debug)]
pub enum Preflight {
Ready(PurgePlan),
Refused(Box<Diagnostic>),
}
pub fn preflight(plan: PurgePlan, world: World) -> Preflight {
if world.supervisor_serving && world.managed_units > 0 && !world.force {
return Preflight::Refused(Box::new(supervisor_active(world.managed_units)));
}
Preflight::Ready(plan)
}
pub fn supervisor_active(managed_units: usize) -> Diagnostic {
Diagnostic::error(
SgCode::PurgeSupervisorActive,
"refused to purge: a supervisor is still managing processes",
)
.note(format!(
"{managed_units} unit(s) are live under the running supervisor; purging its state now would strand them"
))
.help_cmd("stop the supervisor first", "sysg stop --supervisor")
.help_cmd("then purge", "sysg purge")
.help_cmd("or force it (stops + wipes)", "sysg purge --force")
.help_docs()
}
pub fn incomplete(detail: impl Into<String>) -> Diagnostic {
Diagnostic::error(
SgCode::PurgeIncomplete,
"purge removed some state but did not finish; the remaining state may be partial",
)
.note(detail)
.help_cmd("retry the purge", "sysg purge")
.help_docs()
}
pub fn project_not_found(project: &str) -> Diagnostic {
Diagnostic::error(
SgCode::PurgeProjectNotFound,
format!("no state on disk for project '{project}'"),
)
.note("nothing was deleted; check the project id")
.help_cmd("list what has state", "sysg status")
.help_docs()
}
pub fn target_invalid(project: &str) -> Diagnostic {
Diagnostic::error(
SgCode::PurgeTargetInvalid,
format!("'{project}' does not name a single project"),
)
.note("nothing was deleted; a project id is one path segment")
.help_cmd("list what has state", "sysg status")
.help_docs()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_selector_wipes_everything() {
assert_eq!(
resolve_plan(None, None, None).unwrap(),
PurgePlan::Everything
);
}
#[test]
fn config_projects_become_a_config_purge() {
assert_eq!(
resolve_plan(None, None, Some(vec!["a".into(), "b".into()])).unwrap(),
PurgePlan::Config {
projects: vec!["a".into(), "b".into()]
}
);
}
#[test]
fn project_selector_scopes_to_one_project() {
assert_eq!(
resolve_plan(None, Some("demo"), None).unwrap(),
PurgePlan::Project {
project: "demo".into()
}
);
}
#[test]
fn preflight_refuses_a_live_managing_supervisor() {
let world = World {
supervisor_serving: true,
managed_units: 3,
force: false,
};
match preflight(PurgePlan::Everything, world) {
Preflight::Refused(diag) => {
assert_eq!(diag.code, SgCode::PurgeSupervisorActive)
}
other => panic!("expected refusal, got {other:?}"),
}
}
#[test]
fn preflight_allows_force_over_a_live_supervisor() {
let world = World {
supervisor_serving: true,
managed_units: 3,
force: true,
};
assert!(matches!(
preflight(PurgePlan::Everything, world),
Preflight::Ready(_)
));
}
#[test]
fn preflight_allows_a_down_supervisor() {
let world = World {
supervisor_serving: false,
managed_units: 0,
force: false,
};
assert!(matches!(
preflight(PurgePlan::Everything, world),
Preflight::Ready(_)
));
}
#[test]
fn preflight_allows_a_serving_but_empty_supervisor() {
let world = World {
supervisor_serving: true,
managed_units: 0,
force: false,
};
assert!(matches!(
preflight(PurgePlan::Everything, world),
Preflight::Ready(_)
));
}
}