use super::census::{DriftCensus, SkipReason};
use super::ignore::should_ignore_drift;
use super::{DriftFinding, DRIFT_QUERY_TIMEOUT_SECS};
use crate::core::types::{Machine, Resource, ResourceStatus, ResourceType, StateLock};
use crate::tripwire::hasher;
use std::path::Path;
pub fn check_file_drift(
resource_id: &str,
path: &str,
expected_hash: &str,
) -> Option<DriftFinding> {
let file_path = Path::new(path);
if !file_path.exists() {
return Some(DriftFinding {
resource_id: resource_id.to_string(),
resource_type: ResourceType::File,
expected_hash: expected_hash.to_string(),
actual_hash: "MISSING".to_string(),
detail: format!("{path} does not exist"),
});
}
let actual = if file_path.is_dir() {
hasher::hash_directory(file_path).unwrap_or_else(|e| format!("ERROR:{e}"))
} else {
hasher::hash_file(file_path).unwrap_or_else(|e| format!("ERROR:{e}"))
};
if actual != expected_hash {
Some(DriftFinding {
resource_id: resource_id.to_string(),
resource_type: ResourceType::File,
expected_hash: expected_hash.to_string(),
actual_hash: actual,
detail: format!("{path} content changed"),
})
} else {
None
}
}
fn hash_remote_content(
out: &crate::transport::ExecOutput,
path: &str,
machine: &Machine,
) -> Option<String> {
if out.stdout.trim() == "__DIR__" {
let ls_script = format!("ls -la '{path}'");
match crate::transport::exec_script_timeout(
machine,
&ls_script,
Some(DRIFT_QUERY_TIMEOUT_SECS),
) {
Ok(ls_out) if ls_out.success() => Some(hasher::hash_string_or_sentinel(&ls_out.stdout)),
_ => None,
}
} else {
Some(hasher::hash_string_or_sentinel(&out.stdout))
}
}
fn file_drift_finding(
resource_id: &str,
expected_hash: &str,
actual_hash: String,
detail: String,
) -> DriftFinding {
DriftFinding {
resource_id: resource_id.to_string(),
resource_type: ResourceType::File,
expected_hash: expected_hash.to_string(),
actual_hash,
detail,
}
}
pub fn check_file_drift_via_transport(
resource_id: &str,
path: &str,
expected_hash: &str,
machine: &Machine,
) -> Option<DriftFinding> {
let script = format!(
"set -euo pipefail\nif [ -d '{path}' ]; then echo '__DIR__'; else cat '{path}'; fi"
);
match crate::transport::exec_script_timeout(machine, &script, Some(DRIFT_QUERY_TIMEOUT_SECS)) {
Ok(out) if out.success() => {
let actual = hash_remote_content(&out, path, machine)?;
if actual != expected_hash {
Some(file_drift_finding(
resource_id,
expected_hash,
actual,
format!("{path} content changed"),
))
} else {
None
}
}
Ok(out) => Some(file_drift_finding(
resource_id,
expected_hash,
"MISSING".to_string(),
format!("{} not accessible: {}", path, out.stderr.trim()),
)),
Err(e) => Some(file_drift_finding(
resource_id,
expected_hash,
"ERROR".to_string(),
format!("transport error: {e}"),
)),
}
}
pub(super) fn detect_drift_with_lifecycle(
lock: &StateLock,
machine: Option<&Machine>,
resources: &indexmap::IndexMap<String, Resource>,
census: &mut DriftCensus,
) -> Vec<DriftFinding> {
let mut findings = Vec::new();
for (id, rl) in &lock.resources {
if rl.resource_type != ResourceType::File {
continue;
}
if rl.status != ResourceStatus::Converged {
census.skipped(id, &rl.resource_type, SkipReason::NotConverged);
continue;
}
if should_ignore_drift(id, resources) {
census.skipped(id, &rl.resource_type, SkipReason::IgnoreDrift);
continue;
}
let Some((path, expected)) = locked_file_target(rl) else {
census.skipped(id, &rl.resource_type, SkipReason::NoLockedHash);
continue;
};
census.inspected(id, &rl.resource_type);
if let Some(f) = check_file_resource_drift(id, path, expected, machine) {
findings.push(f);
}
}
findings
}
pub(super) fn locked_file_target(rl: &crate::core::types::ResourceLock) -> Option<(&str, &str)> {
let path = match rl.details.get("path") {
Some(serde_yaml_ng::Value::String(s)) => s.as_str(),
_ => return None,
};
let expected = match rl.details.get("content_hash") {
Some(serde_yaml_ng::Value::String(s)) => s.as_str(),
_ => return None,
};
Some((path, expected))
}
pub(super) fn check_file_resource_drift(
id: &str,
path: &str,
expected: &str,
machine: Option<&Machine>,
) -> Option<DriftFinding> {
match machine {
Some(m) if !crate::transport::is_local_addr(&m.addr) => {
check_file_drift_via_transport(id, path, expected, m)
}
_ => check_file_drift(id, path, expected),
}
}
pub(super) fn detect_drift_impl(
lock: &StateLock,
machine: Option<&Machine>,
census: &mut DriftCensus,
) -> Vec<DriftFinding> {
detect_drift_with_lifecycle(lock, machine, &indexmap::IndexMap::new(), census)
}