use super::census::{DriftCensus, SkipReason};
use super::ignore::should_ignore_drift;
use super::unmeasured::{self, Reading};
use super::DriftFinding;
use crate::core::types::{Machine, Resource, ResourceStatus, ResourceType, StateLock};
pub(super) fn detect_image_drift(
lock: &StateLock,
machine: &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::Image {
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(expected_digest) = rl.detail_str("manifest_digest") else {
census.skipped(id, &rl.resource_type, SkipReason::NoLockedHash);
continue;
};
let Some(container_name) = rl.detail_str("container_name") else {
census.skipped(id, &rl.resource_type, SkipReason::NoLockedHash);
continue;
};
census.inspected(id, &rl.resource_type);
if let Some(f) = check_image_drift(id, container_name, expected_digest, machine) {
findings.push(f);
}
}
findings
}
pub fn check_image_drift(
resource_id: &str,
container_name: &str,
expected_digest: &str,
machine: &Machine,
) -> Option<DriftFinding> {
let script = format!(
"docker inspect {container_name} --format '{{{{.Image}}}}' 2>/dev/null || echo 'NOT_RUNNING'"
);
let out = match unmeasured::read(machine, &script) {
Reading::Answered(out) => out,
Reading::Unmeasured(why) => {
return Some(DriftFinding::unmeasured(
resource_id,
ResourceType::Image,
expected_digest,
why,
))
}
};
if !out.success() {
return Some(DriftFinding {
resource_id: resource_id.to_string(),
resource_type: ResourceType::Image,
expected_hash: expected_digest.to_string(),
actual_hash: "ERROR".to_string(),
detail: format!("docker inspect failed: {}", out.stderr.trim()),
});
}
let actual = out.stdout.trim().to_string();
if actual == "NOT_RUNNING" {
Some(DriftFinding {
resource_id: resource_id.to_string(),
resource_type: ResourceType::Image,
expected_hash: expected_digest.to_string(),
actual_hash: "NOT_RUNNING".to_string(),
detail: format!("container {container_name} is not running"),
})
} else if actual != expected_digest {
Some(DriftFinding {
resource_id: resource_id.to_string(),
resource_type: ResourceType::Image,
expected_hash: expected_digest.to_string(),
actual_hash: actual,
detail: "deployed image differs from built image".to_string(),
})
} else {
None
}
}