use super::census::{DriftCensus, SkipReason};
use super::ignore::should_ignore_drift;
use super::task_check::{self, DriftOptions};
use super::DriftReport;
use crate::core::types::{Machine, Resource, ResourceType};
pub fn detect_drift_lockless(
machine_name: &str,
machine: &Machine,
resources: &indexmap::IndexMap<String, Resource>,
opts: DriftOptions,
) -> DriftReport {
let mut census = DriftCensus::new();
let mut findings = Vec::new();
for (id, resource) in resources {
if !targets(resource, machine_name) {
continue;
}
if let Some(reason) = skip_reason(id, resource, resources, opts) {
census.skipped(id, &resource.resource_type, reason);
continue;
}
census.inspected(id, &resource.resource_type);
if let Some(f) = task_check::check_task_drift(id, resource, machine) {
findings.push(f);
}
}
DriftReport { findings, census }
}
fn targets(resource: &Resource, machine_name: &str) -> bool {
resource.resource_type != ResourceType::Recipe
&& resource.machine.iter().any(|m| m == machine_name)
}
fn skip_reason(
id: &str,
resource: &Resource,
resources: &indexmap::IndexMap<String, Resource>,
opts: DriftOptions,
) -> Option<SkipReason> {
if !task_check::owns(resource) {
return Some(SkipReason::NoLock);
}
if should_ignore_drift(id, resources) {
return Some(SkipReason::IgnoreDrift);
}
if !opts.run_task_checks {
return Some(SkipReason::TaskChecksDisabled);
}
None
}
pub fn lockless_dry_run_ids(
machine_name: &str,
resources: &indexmap::IndexMap<String, Resource>,
opts: DriftOptions,
) -> Vec<String> {
resources
.iter()
.filter(|(id, r)| targets(r, machine_name) && skip_reason(id, r, resources, opts).is_none())
.map(|(id, _)| id.clone())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::{MachineTarget, ResourceType};
fn machine() -> Machine {
serde_yaml_ng::from_str("hostname: sandbox\naddr: 127.0.0.1").unwrap()
}
fn task(check: &str) -> Resource {
Resource {
resource_type: ResourceType::Task,
machine: MachineTarget::Single("sandbox".to_string()),
command: Some("exit 1".to_string()),
completion_check: Some(check.to_string()),
..Default::default()
}
}
fn file() -> Resource {
Resource {
resource_type: ResourceType::File,
machine: MachineTarget::Single("sandbox".to_string()),
path: Some("/tmp/forjar-385-unit".to_string()),
content: Some("x\n".to_string()),
..Default::default()
}
}
fn resources(pairs: Vec<(&str, Resource)>) -> indexmap::IndexMap<String, Resource> {
pairs
.into_iter()
.map(|(id, r)| (id.to_string(), r))
.collect()
}
#[test]
fn a_satisfied_assertion_is_inspected_and_clean() {
let res = resources(vec![("guard", task("true"))]);
let report = detect_drift_lockless("sandbox", &machine(), &res, DriftOptions::default());
assert!(report.findings.is_empty());
assert_eq!(report.census.inspected_total(), 1);
assert_eq!(report.census.skipped_total(), 0);
}
#[test]
fn a_violated_assertion_is_drift_without_a_lock() {
let res = resources(vec![("guard", task("false"))]);
let report = detect_drift_lockless("sandbox", &machine(), &res, DriftOptions::default());
assert_eq!(report.findings.len(), 1);
assert_eq!(report.findings[0].resource_id, "guard");
}
#[test]
fn a_baseline_resource_is_skipped_as_no_lock() {
let res = resources(vec![("guard", task("true")), ("hosts", file())]);
let report = detect_drift_lockless("sandbox", &machine(), &res, DriftOptions::default());
assert_eq!(report.census.in_scope(), 2);
assert_eq!(report.census.inspected_total(), 1);
assert_eq!(
report
.census
.skipped_by_reason()
.get("no lock (never applied from here)"),
Some(&1)
);
}
#[test]
fn a_resource_for_another_machine_is_out_of_scope() {
let mut elsewhere = task("false");
elsewhere.machine = MachineTarget::Single("other".to_string());
let res = resources(vec![("guard", elsewhere)]);
let report = detect_drift_lockless("sandbox", &machine(), &res, DriftOptions::default());
assert_eq!(report.census.in_scope(), 0);
assert!(report.findings.is_empty());
}
#[test]
fn no_task_checks_is_reported_not_silent() {
let res = resources(vec![("guard", task("false"))]);
let opts = DriftOptions {
run_task_checks: false,
};
let report = detect_drift_lockless("sandbox", &machine(), &res, opts);
assert!(report.findings.is_empty());
assert_eq!(
report.census.skipped_by_reason().get("--no-task-checks"),
Some(&1)
);
}
#[test]
fn the_dry_run_ids_are_the_ones_the_run_executes() {
let res = resources(vec![("guard", task("true")), ("hosts", file())]);
let ids = lockless_dry_run_ids("sandbox", &res, DriftOptions::default());
assert_eq!(ids, vec!["guard".to_string()]);
}
}