use crate::drift::{DriftItem, DriftKind, DriftReport, Severity};
use crate::manifest::Manifest;
use crate::ping::{PingResult, PingStatus};
use colored::Colorize;
use std::collections::HashSet;
pub fn render_check(report: &DriftReport, ping_results: &[PingResult]) {
let errors = report.error_count();
let warnings = report.warning_count();
println!(
"{}",
format!(
"svccat: {} declared, {} discovered [{}]",
report.declared, report.discovered, report.manifest
)
.bold()
);
println!();
if report.drifts.is_empty() {
println!("{}", " OK No drift detected".green().bold());
} else {
println!(
"{}",
format!(
" DRIFT DETECTED ({} error{}, {} warning{})",
errors,
if errors == 1 { "" } else { "s" },
warnings,
if warnings == 1 { "" } else { "s" }
)
.yellow()
.bold()
);
println!();
for item in &report.drifts {
print_drift_item(item);
}
println!();
if errors > 0 {
println!("{}", format!(" x {} error(s)", errors).red().bold());
}
if warnings > 0 {
println!("{}", format!(" ! {} warning(s)", warnings).yellow());
}
}
if !ping_results.is_empty() {
println!();
render_ping_results(ping_results);
}
}
pub fn render_since_diff(
old_report: &DriftReport,
new_report: &DriftReport,
git_ref: &str,
) -> (usize, usize) {
let old_keys: HashSet<String> = old_report.drifts.iter().map(drift_key).collect();
let new_keys: HashSet<String> = new_report.drifts.iter().map(drift_key).collect();
let added: Vec<&DriftItem> = new_report
.drifts
.iter()
.filter(|d| !old_keys.contains(&drift_key(d)))
.collect();
let resolved: Vec<&DriftItem> = old_report
.drifts
.iter()
.filter(|d| !new_keys.contains(&drift_key(d)))
.collect();
let unchanged = new_report.drifts.len().saturating_sub(added.len());
println!(
"{}",
format!("svccat --since {} [{}]", git_ref, new_report.manifest).bold()
);
println!();
if added.is_empty() && resolved.is_empty() {
println!(
"{}",
format!(
" OK No change in drift since {} ({} existing item{})",
git_ref,
unchanged,
if unchanged == 1 { "" } else { "s" }
)
.green()
.bold()
);
return (0, 0);
}
if !added.is_empty() {
println!(
"{}",
format!(" NEW drift since {} ({}):", git_ref, added.len())
.yellow()
.bold()
);
println!();
for item in &added {
print_drift_item(item);
}
println!();
}
if !resolved.is_empty() {
println!(
"{}",
format!(" RESOLVED since {} ({}):", git_ref, resolved.len())
.green()
.bold()
);
println!();
for item in &resolved {
print_resolved_drift_item(item);
}
println!();
}
if unchanged > 0 {
println!(
" {} existing drift item{} unchanged",
unchanged,
if unchanged == 1 { "" } else { "s" }
);
}
(added.len(), resolved.len())
}
pub fn render_ping_results(results: &[PingResult]) {
println!("{}", " PING RESULTS".bold());
println!();
for r in results {
match &r.ping {
PingStatus::Reachable { code } if *code < 400 => {
println!(
" {} {} {} ({})",
"✓".green().bold(),
"[PING]".green(),
r.service,
code
);
}
PingStatus::Reachable { code } => {
println!(
" {} {} {} {} ({})",
"!".yellow().bold(),
"[PING]".yellow(),
r.service,
r.url,
code
);
}
PingStatus::Unreachable { reason } => {
println!(
" {} {} {} {} — {}",
"x".red().bold(),
"[PING]".red(),
r.service,
r.url,
reason
);
}
}
}
}
fn print_drift_item(item: &DriftItem) {
let icon = match item.severity {
Severity::Error => "x".red().bold(),
Severity::Warning => "!".yellow().bold(),
};
let kind_label = drift_kind_label(&item.kind);
println!(" {} {} {}", icon, kind_label, item.message);
}
fn print_resolved_drift_item(item: &DriftItem) {
let kind_label = drift_kind_label(&item.kind);
println!(" {} {} {}", "✓".green().bold(), kind_label, item.message);
}
fn drift_kind_label(kind: &DriftKind) -> colored::ColoredString {
match kind {
DriftKind::DeclaredMissingFromRepo => "[MISSING] ".red(),
DriftKind::UndeclaredInRepo => "[UNDECLARED]".yellow(),
DriftKind::MissingField => "[FIELD] ".cyan(),
DriftKind::MissingReferencedFile => "[REF] ".yellow(),
DriftKind::PolicyViolation => "[POLICY] ".red(),
DriftKind::DanglingDependency => "[DEPENDS] ".red(),
DriftKind::CircularDependency => "[CYCLE] ".red(),
}
}
fn drift_key(item: &DriftItem) -> String {
format!(
"{:?}|{}|{}",
item.kind,
item.service,
item.detail.as_deref().unwrap_or("")
)
}
pub fn render_compact(manifest: &Manifest, report: &DriftReport) {
println!(
"{}",
format!(
"svccat: {} declared, {} discovered [{}]",
report.declared, report.discovered, report.manifest
)
.bold()
);
println!();
let mut drift_by_service: std::collections::HashMap<&str, Vec<&DriftItem>> =
std::collections::HashMap::new();
let mut undeclared: Vec<&DriftItem> = Vec::new();
for item in &report.drifts {
if item.kind == DriftKind::UndeclaredInRepo {
undeclared.push(item);
} else {
drift_by_service
.entry(item.service.as_str())
.or_default()
.push(item);
}
}
let mut ok = 0usize;
let mut errors = 0usize;
let mut warnings = 0usize;
for svc in &manifest.services {
let drifts = drift_by_service.get(svc.name.as_str());
match drifts {
None => {
println!(" {} {}", "✓".green().bold(), svc.name);
ok += 1;
}
Some(items) => {
let worst = items
.iter()
.find(|d| d.severity == Severity::Error)
.or_else(|| items.first());
if let Some(d) = worst {
let (icon, label) = match d.severity {
Severity::Error => {
errors += 1;
("✗".red().bold(), drift_kind_label(&d.kind))
}
Severity::Warning => {
warnings += 1;
("!".yellow().bold(), drift_kind_label(&d.kind))
}
};
let extra = if items.len() > 1 {
format!(" (+{})", items.len() - 1)
} else {
String::new()
};
println!(
" {} {} {}{}",
icon,
label,
svc.name,
extra
);
}
}
}
}
for item in &undeclared {
println!(
" {} {} {}",
"!".yellow().bold(),
drift_kind_label(&item.kind),
item.service,
);
warnings += 1;
}
println!();
if errors == 0 && warnings == 0 {
println!("{}", format!(" {} ok, 0 errors, 0 warnings", ok).green().bold());
} else {
println!(
" {} ok {} {}",
ok,
format!("{} error(s)", errors).red().bold(),
format!("{} warning(s)", warnings).yellow()
);
}
}