use crate::{discovery, drift, lint, manifest, ping};
use anyhow::Result;
use colored::Colorize;
use std::path::Path;
pub struct AuditResult {
pub manifest_path: String,
pub lint_errors: usize,
pub lint_warnings: usize,
pub drift_errors: usize,
pub drift_warnings: usize,
pub ping_failures: usize,
pub score: u32,
pub passed: bool,
}
pub fn run(
manifest_path: &Path,
root: &Path,
ignore: &[String],
depth: u32,
do_ping: bool,
) -> Result<(AuditResult, lint::LintResult, drift::DriftReport, Vec<ping::PingResult>)> {
let m = manifest::Manifest::load(manifest_path)?;
let lint_result = lint::run(&m);
let lint_errors = lint_result.error_count();
let lint_warnings = lint_result.warning_count();
let discovered = discovery::discover_services_with_opts(root, &m, ignore, depth);
let mut report = drift::analyze(&m, &discovered, root);
report.manifest = manifest_path.display().to_string();
let drift_errors = report.error_count();
let drift_warnings = report.warning_count();
let ping_results = if do_ping {
ping::ping_services(&m)
} else {
vec![]
};
let ping_failures = ping_results.iter().filter(|p| !p.is_ok()).count();
let deductions = (drift_errors * 10)
+ (drift_warnings * 3)
+ (lint_errors * 5)
+ (lint_warnings * 2)
+ (ping_failures * 5);
let score = 100u32.saturating_sub(deductions as u32);
let passed = drift_errors == 0 && lint_errors == 0;
let result = AuditResult {
manifest_path: manifest_path.display().to_string(),
lint_errors,
lint_warnings,
drift_errors,
drift_warnings,
ping_failures,
score,
passed,
};
Ok((result, lint_result, report, ping_results))
}
pub fn render_terminal(
result: &AuditResult,
lint_result: &lint::LintResult,
drift_report: &drift::DriftReport,
ping_results: &[ping::PingResult],
) {
let total_lint = result.lint_errors + result.lint_warnings;
let total_drift = result.drift_errors + result.drift_warnings;
println!(
"{}",
format!("svccat audit [{}]", result.manifest_path).bold()
);
println!();
let lint_header = if total_lint == 0 {
format!("Lint {}", "OK".green().bold())
} else {
format!(
"Lint {} error{}, {} warning{}",
result.lint_errors,
plural(result.lint_errors),
result.lint_warnings,
plural(result.lint_warnings)
)
};
println!("{}", lint_header.bold());
for issue in &lint_result.issues {
let (icon, msg) = match issue.severity {
lint::LintSeverity::Error => ("✗".red().bold(), issue.message.as_str().red()),
lint::LintSeverity::Warning => ("⚠".yellow(), issue.message.as_str().yellow()),
};
println!(" {icon} {msg}");
}
if lint_result.issues.is_empty() {
println!(" {} No issues found", "✓".green().bold());
}
println!();
let drift_header = if total_drift == 0 {
format!("Drift {}", "OK".green().bold())
} else {
format!(
"Drift {} error{}, {} warning{}",
result.drift_errors,
plural(result.drift_errors),
result.drift_warnings,
plural(result.drift_warnings)
)
};
println!("{}", drift_header.bold());
for item in &drift_report.drifts {
let (icon, line) = match item.severity {
drift::Severity::Error => (
"✗".red().bold(),
format!("{}: {:?} - {}", item.service, item.kind, item.message).red(),
),
drift::Severity::Warning => (
"⚠".yellow(),
format!("{}: {:?} - {}", item.service, item.kind, item.message).yellow(),
),
};
println!(" {icon} {line}");
}
if drift_report.drifts.is_empty() {
println!(" {} No drift detected", "✓".green().bold());
}
println!();
if !ping_results.is_empty() {
let reachable = ping_results.iter().filter(|p| p.is_ok()).count();
let unreachable = ping_results.len() - reachable;
let ping_header = if unreachable == 0 {
format!("Ping {} ({}/{} reachable)", "OK".green().bold(), reachable, ping_results.len())
} else {
format!("Ping {}/{} reachable", reachable, ping_results.len())
};
println!("{}", ping_header.bold());
for p in ping_results {
if !p.is_ok() {
println!(
" {} {} {} {}",
"✗".red().bold(),
p.service.as_str().red(),
p.url.as_str(),
"UNREACHABLE".red()
);
}
}
if unreachable == 0 {
println!(" {} All services reachable", "✓".green().bold());
}
println!();
}
let bar_width = 20usize;
let filled = (result.score as usize * bar_width) / 100;
let bar = format!("{}{}", "█".repeat(filled), "░".repeat(bar_width - filled));
let score_label = format!("Score: {}/100 {}", result.score, bar);
let verdict = if result.passed {
"PASS".green().bold()
} else {
"FAIL".red().bold()
};
println!(" {} {verdict}", score_label.bold());
if !result.passed {
println!();
if result.drift_errors > 0 {
println!(
" Tip: run {} to auto-remediate drift.",
"`svccat fix`".cyan()
);
}
}
}
pub fn render_json(result: &AuditResult) -> anyhow::Result<()> {
let payload = serde_json::json!({
"manifest": result.manifest_path,
"lint_errors": result.lint_errors,
"lint_warnings": result.lint_warnings,
"drift_errors": result.drift_errors,
"drift_warnings": result.drift_warnings,
"ping_failures": result.ping_failures,
"score": result.score,
"passed": result.passed,
});
println!("{}", serde_json::to_string_pretty(&payload)?);
Ok(())
}
fn plural(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}