use crate::core::types::ResourceType;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SkipReason {
NotConverged,
IgnoreDrift,
NoObservedState,
NotInConfig,
NotInLock,
NoLockedHash,
NoConfigLoaded,
TaskChecksDisabled,
ObservationMaskChanged,
NoLock,
}
impl SkipReason {
pub fn as_str(self) -> &'static str {
match self {
Self::NotConverged => "not converged in the lock",
Self::IgnoreDrift => "lifecycle.ignore_drift",
Self::NoObservedState => "no observed state in the lock",
Self::NotInConfig => "in the lock, not in the config",
Self::NotInLock => "declared here, absent from the lock",
Self::NoLockedHash => "no hash recorded in the lock",
Self::NoConfigLoaded => "no config loaded (file hashes only)",
Self::TaskChecksDisabled => "--no-task-checks",
Self::ObservationMaskChanged => "ignore_drift changed since the baseline",
Self::NoLock => "no lock (never applied from here)",
}
}
}
#[derive(Debug, Clone)]
struct Entry {
resource_type: ResourceType,
skipped: Option<SkipReason>,
}
#[derive(Debug, Clone, Default)]
pub struct DriftCensus {
entries: BTreeMap<String, Entry>,
}
impl DriftCensus {
pub fn new() -> Self {
Self::default()
}
pub(super) fn inspected(&mut self, id: &str, resource_type: &ResourceType) {
self.entries.insert(
id.to_string(),
Entry {
resource_type: resource_type.clone(),
skipped: None,
},
);
}
pub(super) fn skipped(&mut self, id: &str, resource_type: &ResourceType, reason: SkipReason) {
self.entries.entry(id.to_string()).or_insert_with(|| Entry {
resource_type: resource_type.clone(),
skipped: Some(reason),
});
}
pub fn in_scope(&self) -> usize {
self.entries.len()
}
pub fn inspected_total(&self) -> usize {
self.entries
.values()
.filter(|e| e.skipped.is_none())
.count()
}
pub fn skipped_total(&self) -> usize {
self.in_scope() - self.inspected_total()
}
pub fn inspected_by_type(&self) -> BTreeMap<String, usize> {
let mut counts = BTreeMap::new();
for entry in self.entries.values().filter(|e| e.skipped.is_none()) {
*counts
.entry(entry.resource_type.to_string())
.or_insert(0usize) += 1;
}
counts
}
pub fn skipped_by_reason(&self) -> BTreeMap<&'static str, usize> {
let mut counts = BTreeMap::new();
for reason in self.entries.values().filter_map(|e| e.skipped) {
*counts.entry(reason.as_str()).or_insert(0usize) += 1;
}
counts
}
pub fn skipped_ids(&self, reason: SkipReason) -> Vec<&str> {
self.entries
.iter()
.filter(|(_, e)| e.skipped == Some(reason))
.map(|(id, _)| id.as_str())
.collect()
}
pub fn summary_lines(&self) -> Vec<String> {
let mut lines = vec![format!(
"inspected {} of {} resource(s) in scope: {}",
self.inspected_total(),
self.in_scope(),
render_counts(self.inspected_by_type().into_iter())
)];
if self.skipped_total() > 0 {
lines.push(format!(
"skipped {}: {}",
self.skipped_total(),
render_counts(self.skipped_by_reason().into_iter())
));
}
lines
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"in_scope": self.in_scope(),
"inspected": self.inspected_total(),
"skipped": self.skipped_total(),
"inspected_by_type": self.inspected_by_type(),
"skipped_by_reason": self.skipped_by_reason(),
})
}
}
fn render_counts<K: std::fmt::Display>(counts: impl Iterator<Item = (K, usize)>) -> String {
let rendered: Vec<String> = counts.map(|(k, v)| format!("{k} {v}")).collect();
if rendered.is_empty() {
"none".to_string()
} else {
rendered.join(", ")
}
}