use super::drift::{print_dry_run_report, print_machine_header, report_machine_findings};
use super::drift::{DriftScan, ScanOptions};
use crate::core::types;
use crate::tripwire::drift;
use std::path::Path;
pub(super) fn scan_lockless(
state_dir: &Path,
machine_filter: Option<&str>,
config: Option<&types::ForjarConfig>,
scan_opts: ScanOptions,
) -> Result<DriftScan, String> {
let cfg = require_config(state_dir, config)?;
let names = select_machines(cfg, machine_filter)?;
let resolved = resolve(cfg);
let mut scan = DriftScan::default();
for name in names {
let Some(machine) = cfg.machines.get(&name) else {
continue;
};
print_machine_header(&name, SCOPE_NOTE, scan_opts);
let report = drift::detect_drift_lockless(&name, machine, &resolved, scan_opts.detect);
report_machine_findings(&name, report, &mut scan, scan_opts);
scan.machines_checked += 1;
}
Ok(scan)
}
pub(super) fn dry_run_lockless(
state_dir: &Path,
machine_filter: Option<&str>,
config: Option<&types::ForjarConfig>,
json: bool,
opts: drift::DriftOptions,
) -> Result<(), String> {
let cfg = require_config(state_dir, config)?;
let names = select_machines(cfg, machine_filter)?;
let resolved = resolve(cfg);
let mut checks: Vec<serde_json::Value> = Vec::new();
let mut total = 0usize;
for name in names {
let ids = drift::lockless_dry_run_ids(&name, &resolved, opts);
if !json {
println!(
"Machine: {} ({}, {} checkable)",
name,
SCOPE_NOTE,
ids.len()
);
}
for id in &ids {
if json {
checks.push(serde_json::json!({
"machine": name,
"resource": id,
"status": "no lock",
"hash": serde_json::Value::Null,
}));
} else {
println!(" would check: {id} (completion_check; no lock)");
}
}
total += ids.len();
}
print_dry_run_report(json, total, &checks)
}
const SCOPE_NOTE: &str = "no lock — assertions only";
fn resolve(cfg: &types::ForjarConfig) -> indexmap::IndexMap<String, types::Resource> {
crate::core::resolver::resolve_all(&cfg.resources, &cfg.params, &cfg.machines, &cfg.secrets)
}
fn require_config<'a>(
state_dir: &Path,
config: Option<&'a types::ForjarConfig>,
) -> Result<&'a types::ForjarConfig, String> {
config.ok_or_else(|| {
format!(
"no lock and no config: {} does not exist and no config was loaded, \
so NOTHING was measured. An absent state dir alone is survivable — \
forjar can still execute the completion_check of every `type: task` \
a config declares — but with neither there is no assertion to run \
and no baseline to compare. Point -f at a config, or --state-dir at \
the directory an apply wrote.",
state_dir.display()
)
})
}
fn select_machines(
cfg: &types::ForjarConfig,
machine_filter: Option<&str>,
) -> Result<Vec<String>, String> {
let Some(filter) = machine_filter else {
return Ok(cfg.machines.keys().cloned().collect());
};
if !cfg.machines.contains_key(filter) {
let known: Vec<&str> = cfg.machines.keys().map(String::as_str).collect();
return Err(format!(
"unknown machine '{filter}' — there is no state directory and the \
config does not declare it, so NOTHING was checked. Known: {}",
if known.is_empty() {
"(none)".to_string()
} else {
known.join(", ")
}
));
}
Ok(vec![filter.to_string()])
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg(yaml: &str) -> types::ForjarConfig {
serde_yaml_ng::from_str(yaml).expect("config")
}
fn two_machines() -> types::ForjarConfig {
cfg("version: '1.0'\nname: t\nmachines:\n a:\n hostname: a\n addr: 127.0.0.1\n b:\n hostname: b\n addr: 127.0.0.1\nresources: {}\n")
}
#[test]
fn no_filter_selects_every_declared_machine() {
let c = two_machines();
assert_eq!(select_machines(&c, None).unwrap(), vec!["a", "b"]);
}
#[test]
fn a_filter_selects_exactly_one() {
let c = two_machines();
assert_eq!(select_machines(&c, Some("b")).unwrap(), vec!["b"]);
}
#[test]
fn an_unknown_machine_is_refused_and_names_what_exists() {
let c = two_machines();
let err = select_machines(&c, Some("bb")).unwrap_err();
assert!(err.contains("unknown machine 'bb'"), "{err}");
assert!(err.contains("Known: a, b"), "{err}");
}
#[test]
fn no_config_is_a_refusal() {
let err = require_config(Path::new("/nope/state"), None).unwrap_err();
assert!(err.contains("NOTHING was measured"), "{err}");
assert!(err.contains("/nope/state"), "{err}");
}
}