use anyhow::Result;
use clap::{CommandFactory, Parser};
use clap_complete::generate;
use std::io;
use std::process;
use svccat::cli::{
AuditFormat, CiFormat, Cli, Commands, DepsFormat, DiffFormat, ExportFormat, GraphFormat,
HookKind, ImportSource, OutputFormat, PolicyFormat, ReportFormat, ScorecardFormat,
SnapshotAction, TagAction, WorkspaceAction,
};
use svccat::{
audit, ci, config, demo, deps, diff, discovery, drift, fix, hooks, import, init, lint,
manifest, output, ping, policy, report, scorecard, search, serve, since, snapshot, stats, tag,
watch, webhook, workspace,
};
fn main() {
let worker = std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn(run)
.expect("failed to spawn worker thread");
match worker.join() {
Ok(Ok(code)) => process::exit(code),
Ok(Err(e)) => {
eprintln!("error: {e:#}");
process::exit(2);
}
Err(_) => process::exit(101), }
}
fn render_check_output_to_string(
format: &OutputFormat,
report: &drift::DriftReport,
ping_results: &[ping::PingResult],
) -> Result<Option<String>> {
let maybe_string = match format {
OutputFormat::Json => Some(output::json::render_check_to_string(report, ping_results)?),
OutputFormat::Markdown => Some(output::markdown::render_check_markdown(
report,
ping_results,
)),
OutputFormat::Csv => Some(output::csv::render_check_to_string(report)),
OutputFormat::Slack => Some(output::slack::render_check_to_string(report)?),
OutputFormat::Teams => Some(output::teams::render_check_to_string(report)?),
OutputFormat::Datadog => Some(output::datadog::render_check_to_string(report)?),
_ => None,
};
Ok(maybe_string)
}
fn run() -> Result<i32> {
let cli = Cli::parse();
let root = cli.root.unwrap_or_else(|| std::path::PathBuf::from("."));
let cfg = config::SvccatConfig::load(&root)?;
match cli.command {
Commands::Check {
manifest: manifest_path,
format,
fail_on_drift,
ping: do_ping,
ignore: cli_ignore,
team,
since,
fail_on_new_drift,
depth,
baseline,
summary,
output: output_path,
} => {
let format = if format == OutputFormat::Terminal
&& std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true")
{
OutputFormat::GithubAnnotation
} else {
format
};
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
if !path.exists() {
eprintln!(
"warning: manifest not found at {} — skipping check",
path.display()
);
return Ok(0);
}
let full_m = manifest::Manifest::load(&path)?;
let mut m = full_m.clone();
if let Some(ref t) = team {
m.services.retain(|s| {
s.team
.as_deref()
.map(|v| v.eq_ignore_ascii_case(t))
.unwrap_or(false)
});
}
let mut ignore: Vec<String> = cfg.ignore.clone();
ignore.extend(cli_ignore);
let discovered_all =
discovery::discover_services_with_opts(&root, &full_m, &ignore, depth);
let in_scope_names: std::collections::HashSet<&str> =
m.services.iter().map(|s| s.name.as_str()).collect();
let other_declared_names: std::collections::HashSet<&str> = full_m
.services
.iter()
.filter(|s| !in_scope_names.contains(s.name.as_str()))
.map(|s| s.name.as_str())
.collect();
let discovered: Vec<_> = discovered_all
.into_iter()
.filter(|d| !other_declared_names.contains(d.name.as_str()))
.collect();
let mut report = drift::analyze(&m, &discovered, &root);
report.manifest = path.display().to_string();
if let Some(ref baseline_path) = baseline {
use std::collections::HashSet;
#[derive(serde::Deserialize)]
struct BaselineFile {
drift: Vec<drift::DriftItem>,
}
let text = std::fs::read_to_string(baseline_path).map_err(|e| {
anyhow::anyhow!("cannot read baseline {}: {e}", baseline_path.display())
})?;
let snap: BaselineFile = serde_json::from_str(&text)
.map_err(|e| anyhow::anyhow!("cannot parse baseline JSON: {e}"))?;
let baseline_keys: HashSet<String> = snap
.drift
.iter()
.map(|d| {
format!(
"{:?}|{}|{}",
d.kind,
d.service,
d.detail.as_deref().unwrap_or("")
)
})
.collect();
report.drifts.retain(|d| {
!baseline_keys.contains(&format!(
"{:?}|{}|{}",
d.kind,
d.service,
d.detail.as_deref().unwrap_or("")
))
});
}
let ping_results = if do_ping {
ping::ping_services(&m)
} else {
vec![]
};
let should_fail = fail_on_drift || cfg.fail_on_drift;
if let Some(ref git_ref) = since {
let old_m = since::load_at_ref(&root, &path, git_ref)?;
let mut old_report = drift::analyze(&old_m, &discovered, &root);
old_report.manifest = path.display().to_string();
let new_count = match format {
OutputFormat::Markdown => {
let md = output::markdown::render_since_diff_markdown(
&old_report,
&report,
git_ref,
);
print!("{}", md);
use std::collections::HashSet;
let old_keys: HashSet<String> = old_report
.drifts
.iter()
.map(|d| {
format!(
"{:?}|{}|{}",
d.kind,
d.service,
d.detail.as_deref().unwrap_or("")
)
})
.collect();
report
.drifts
.iter()
.filter(|d| {
let k = format!(
"{:?}|{}|{}",
d.kind,
d.service,
d.detail.as_deref().unwrap_or("")
);
!old_keys.contains(&k)
})
.count()
}
OutputFormat::GithubAnnotation => {
output::github_annotation::render_since_annotations(&old_report, &report)
}
OutputFormat::Junit => {
output::junit::render_since(&old_report, &report, git_ref)
}
_ => {
let (new_count, _) =
output::terminal::render_since_diff(&old_report, &report, git_ref);
new_count
}
};
if fail_on_new_drift && new_count > 0 {
return Ok(1);
}
} else {
if summary {
println!("manifest: {}", report.manifest);
println!("declared: {}", report.declared);
println!("discovered: {}", report.discovered);
println!(
"drifts: {} ({} errors, {} warnings)",
report.drifts.len(),
report.error_count(),
report.warning_count()
);
if should_fail && !report.drifts.is_empty() {
return Ok(1);
} else {
return Ok(0);
}
}
let maybe_string = render_check_output_to_string(&format, &report, &ping_results)?;
if let Some(content) = maybe_string {
if let Some(ref out_path) = output_path {
std::fs::write(out_path, &content)?;
eprintln!("wrote output to {}", out_path.display());
} else {
print!("{}", content);
}
} else {
match format {
OutputFormat::Terminal => {
output::terminal::render_check(&report, &ping_results)
}
OutputFormat::Compact => {
output::terminal::render_compact(&m, &report);
}
OutputFormat::Sarif => output::sarif::render_check(&report, &ping_results)?,
OutputFormat::Junit => output::junit::render_check(&report, &ping_results)?,
OutputFormat::GithubAnnotation => {
output::github_annotation::render_check(&report);
}
OutputFormat::Csv => output::csv::render_check(&report),
OutputFormat::Slack => output::slack::render_check(&report)?,
OutputFormat::Teams => output::teams::render_check(&report)?,
OutputFormat::Datadog => output::datadog::render_check(&report)?,
OutputFormat::Json | OutputFormat::Markdown => unreachable!(),
}
}
}
let should_fail = fail_on_drift || cfg.fail_on_drift;
let wh_cfg = webhook::load_config(&root);
let _ = webhook::fire_from_drift(&root, &wh_cfg, &report);
if should_fail && !report.drifts.is_empty() {
Ok(1)
} else {
Ok(0)
}
}
Commands::Graph {
manifest: manifest_path,
format,
team,
filter,
output: output_path,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let mut m = manifest::Manifest::load(&path)?;
if let Some(ref pat) = filter {
let pat_lower = pat.to_lowercase();
m.services
.retain(|s| s.name.to_lowercase().contains(&pat_lower));
}
let content = match format {
GraphFormat::Mermaid => {
output::mermaid::render_graph_filtered_string(&m, team.as_deref())
}
GraphFormat::Markdown => output::mermaid::render_markdown_table_string(&m),
GraphFormat::Dot => output::mermaid::render_dot_string(&m, team.as_deref()),
GraphFormat::Plantuml => {
output::mermaid::render_plantuml_string(&m, team.as_deref())
}
GraphFormat::Html => output::mermaid::render_html_graph(&m, team.as_deref()),
};
if let Some(out_path) = output_path {
std::fs::write(&out_path, &content)?;
eprintln!("wrote graph to {}", out_path.display());
} else {
print!("{}", content);
}
Ok(0)
}
Commands::Export {
manifest: manifest_path,
format,
ignore: cli_ignore,
depth,
since: since_ref,
output: output_path,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let mut m = manifest::Manifest::load(&path)?;
let mut ignore: Vec<String> = cfg.ignore.clone();
ignore.extend(cli_ignore);
let discovered = discovery::discover_services_with_opts(&root, &m, &ignore, depth);
let mut report = drift::analyze(&m, &discovered, &root);
report.manifest = path.display().to_string();
if let Some(ref git_ref) = since_ref {
if let Ok(old_m) = since::load_at_ref(&root, &path, git_ref) {
let old_map: std::collections::HashMap<String, &manifest::ServiceEntry> =
old_m.services.iter().map(|s| (s.name.clone(), s)).collect();
m.services.retain(|svc| {
if let Some(old_svc) = old_map.get(&svc.name) {
svc != *old_svc
} else {
true }
});
let discovered2 =
discovery::discover_services_with_opts(&root, &m, &ignore, depth);
report = drift::analyze(&m, &discovered2, &root);
report.manifest = path.display().to_string();
}
}
match format {
ExportFormat::Json => {
let json = serde_json::to_string_pretty(&serde_json::json!({
"version": m.version,
"manifest": report.manifest,
"summary": {
"declared": report.declared,
"discovered": report.discovered,
"drift_count": report.drifts.len(),
"errors": report.error_count(),
"warnings": report.warning_count(),
},
"services": m.services,
"drift": report.drifts,
}))?;
if let Some(ref out_path) = output_path {
std::fs::write(out_path, json)?;
eprintln!("wrote JSON export to {}", out_path.display());
} else {
println!("{}", json);
}
}
ExportFormat::Markdown => {
let mut out = String::new();
out.push_str("## Services\n\n");
out.push_str("| Name | Language | Platform | Role | URL | Team | On-call | Dependencies | Tags |\n");
out.push_str("|------|----------|----------|------|-----|------|---------|--------------|------|\n");
for svc in &m.services {
out.push_str(&format!(
"| {} | {} | {} | {} | {} | {} | {} | {} | {} |\n",
svc.name,
svc.language.as_deref().unwrap_or("—"),
svc.platform.as_deref().unwrap_or("—"),
svc.role.as_deref().unwrap_or("—"),
svc.url
.as_deref()
.map(|u| format!("[link]({})", u))
.unwrap_or_else(|| "—".to_string()),
svc.team.as_deref().unwrap_or("—"),
svc.oncall.as_deref().unwrap_or("—"),
svc.depends_on.join(", "),
svc.tags.join(", ")
));
}
if !report.drifts.is_empty() {
out.push_str("\n## Drift Report\n\n");
out.push_str("| Severity | Kind | Service | Message |\n");
out.push_str("|----------|------|---------|---------|\n");
for item in &report.drifts {
let severity = format!("{:?}", item.severity).to_lowercase();
let kind = format!("{:?}", item.kind);
out.push_str(&format!(
"| {} | {} | {} | {} |\n",
severity, kind, item.service, item.message
));
}
}
if let Some(ref out_path) = output_path {
std::fs::write(out_path, out)?;
eprintln!("wrote Markdown export to {}", out_path.display());
} else {
print!("{}", out);
}
}
ExportFormat::Csv => {
let csv = output::csv::render_export_to_string(&m);
if let Some(ref out_path) = output_path {
std::fs::write(out_path, csv)?;
eprintln!("wrote CSV export to {}", out_path.display());
} else {
print!("{}", csv);
}
}
ExportFormat::BackstageYaml => {
let yaml = output::backstage::render_export(&m)?;
if let Some(ref out_path) = output_path {
std::fs::write(out_path, yaml)?;
eprintln!(
"wrote Backstage.io component export to {}",
out_path.display()
);
} else {
print!("{}", yaml);
}
}
ExportFormat::SpdxJson => {
let spdx = output::spdx::render_export(&m)?;
if let Some(ref out_path) = output_path {
std::fs::write(out_path, spdx)?;
eprintln!("wrote SPDX JSON export to {}", out_path.display());
} else {
println!("{}", spdx);
}
}
}
Ok(0)
}
Commands::Init { output, force } => {
let output_path = output.unwrap_or_else(|| root.join("services.yaml"));
init::run(&root, output_path, force)?;
Ok(0)
}
Commands::Diff {
before,
after,
format,
} => {
let report = diff::diff_snapshots(&before, &after)?;
match format {
DiffFormat::Terminal => diff::render_diff(&report),
DiffFormat::Markdown => diff::render_diff_markdown(&report),
}
Ok(0)
}
Commands::Watch {
manifest: manifest_path,
fail_on_drift,
team,
ignore: cli_ignore,
depth,
since: watch_since,
notify,
interval,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let mut ignore: Vec<String> = cfg.ignore.clone();
ignore.extend(cli_ignore);
let initial_errors = watch::run(
&path,
&root,
&ignore,
team.as_deref(),
depth,
watch_since.as_deref(),
notify,
interval,
)?;
let should_fail = fail_on_drift || cfg.fail_on_drift;
if should_fail && initial_errors > 0 {
Ok(1)
} else {
Ok(0)
}
}
Commands::Report {
manifest: manifest_path,
format,
output: output_path,
ignore: cli_ignore,
history,
badge,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let m = manifest::Manifest::load(&path)?;
let mut ignore: Vec<String> = cfg.ignore.clone();
ignore.extend(cli_ignore);
let discovered = discovery::discover_services_with_ignore(&root, &m, &ignore);
let mut drift_report = drift::analyze(&m, &discovered, &root);
drift_report.manifest = path.display().to_string();
if badge {
println!("{}", report::render_badge(&drift_report));
return Ok(0);
}
let content = if let Some(n) = history {
report::render_history_markdown(&root, &path, &discovered, n)?
} else {
match format {
ReportFormat::Markdown => report::render_markdown(&m, &drift_report),
ReportFormat::Html => report::render_html(&m, &drift_report),
ReportFormat::Json => report::render_json(&m, &drift_report)?,
}
};
if let Some(out_path) = output_path {
std::fs::write(&out_path, &content)?;
eprintln!("wrote report to {}", out_path.display());
} else {
print!("{}", content);
}
Ok(0)
}
Commands::Lint {
manifest: manifest_path,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let m = manifest::Manifest::load(&path)?;
let result = lint::run(&m);
lint::render(&result);
if result.error_count() > 0 {
Ok(1)
} else {
Ok(0)
}
}
Commands::Import {
from,
output: output_path,
force,
} => {
let out = output_path.unwrap_or_else(|| root.join("services.yaml"));
match from {
ImportSource::Backstage => import::run_backstage(&root, out, force)?,
ImportSource::DockerCompose => import::run_docker_compose(&root, out, force)?,
ImportSource::Openapi => import::run_openapi(&root, out, force)?,
}
Ok(0)
}
Commands::Fix {
manifest: manifest_path,
prune,
dry_run,
ignore: cli_ignore,
depth,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let mut ignore: Vec<String> = cfg.ignore.clone();
ignore.extend(cli_ignore);
fix::run(&path, &root, &ignore, depth, prune, dry_run)?;
Ok(0)
}
Commands::InstallHooks {
hook,
fail_on_drift,
} => {
let hook_name = match hook {
HookKind::PreCommit => "pre-commit",
HookKind::PrePush => "pre-push",
};
hooks::install(&root, hook_name, fail_on_drift)?;
Ok(0)
}
Commands::Policy {
manifest: manifest_path,
format,
fail_on_violations,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let m = manifest::Manifest::load(&path)?;
let policy_cfg = policy::PolicyConfig::load(&root).unwrap_or_default();
if policy_cfg.is_empty() {
eprintln!(
"No policy file found. Create .svccat/policy.yaml to define required/recommended fields."
);
return Ok(0);
}
let result = policy::check(&m, &policy_cfg);
match format {
PolicyFormat::Terminal => policy::render_terminal(&result, &policy_cfg),
PolicyFormat::Json => policy::render_json(&result)?,
}
if fail_on_violations && !result.passed() {
Ok(1)
} else {
Ok(0)
}
}
Commands::Snapshot { action } => match action {
SnapshotAction::Save {
name,
manifest: manifest_path,
ignore: cli_ignore,
depth,
sbom,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let m = manifest::Manifest::load(&path)?;
let mut ignore: Vec<String> = cfg.ignore.clone();
ignore.extend(cli_ignore);
let discovered = discovery::discover_services_with_opts(&root, &m, &ignore, depth);
let mut drift_report = drift::analyze(&m, &discovered, &root);
drift_report.manifest = path.display().to_string();
snapshot::save(&root, &name, &m, &drift_report)?;
if sbom {
let p = snapshot::save_sbom(&root, &name, &m)?;
eprintln!("wrote SPDX SBOM to {}", p.display());
}
Ok(0)
}
SnapshotAction::List => {
let snaps = snapshot::list(&root)?;
snapshot::render_list(&snaps);
Ok(0)
}
SnapshotAction::Delete { name } => {
snapshot::delete(&root, &name)?;
Ok(0)
}
SnapshotAction::Compare {
before,
after,
format,
} => {
let diff_report = snapshot::compare(&root, &before, &after)?;
match format {
DiffFormat::Terminal => diff::render_diff(&diff_report),
DiffFormat::Markdown => diff::render_diff_markdown(&diff_report),
}
Ok(0)
}
SnapshotAction::Diff {
name,
ignore: cli_ignore,
depth,
format,
} => {
let snap = snapshot::load(&root, &name)?;
let manifest_path = manifest::find_default(&root);
let m = manifest::Manifest::load(&manifest_path)?;
let mut ignore: Vec<String> = cfg.ignore.clone();
ignore.extend(cli_ignore);
let discovered = discovery::discover_services_with_opts(&root, &m, &ignore, depth);
let mut current_report = drift::analyze(&m, &discovered, &root);
current_report.manifest = manifest_path.display().to_string();
let after_payload = serde_json::json!({
"services": m.services,
"drift": current_report.drifts,
});
let diff_report =
diff::diff_from_json(&snap.payload, &after_payload, &name, "current")?;
match format {
DiffFormat::Terminal => diff::render_diff(&diff_report),
DiffFormat::Markdown => diff::render_diff_markdown(&diff_report),
}
Ok(0)
}
},
Commands::Audit {
manifest: manifest_path,
format,
ping: do_ping,
cost_estimate,
ignore: cli_ignore,
depth,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let mut ignore: Vec<String> = cfg.ignore.clone();
ignore.extend(cli_ignore);
let (result, lint_result, drift_report, ping_results) =
audit::run(&path, &root, &ignore, depth, do_ping, cost_estimate)?;
match format {
AuditFormat::Terminal => {
audit::render_terminal(&result, &lint_result, &drift_report, &ping_results)
}
AuditFormat::Json => audit::render_json(&result)?,
}
if result.passed {
Ok(0)
} else {
Ok(1)
}
}
Commands::Ci {
manifest: manifest_path,
ignore: cli_ignore,
depth,
format,
watch: do_watch,
interval,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let mut ignore: Vec<String> = cfg.ignore.clone();
ignore.extend(cli_ignore);
if do_watch {
let errors = ci::watch(&path, &root, &ignore, depth, interval)?;
return if errors > 0 { Ok(1) } else { Ok(0) };
}
let m = manifest::Manifest::load(&path)?;
let result = ci::run(&m, &root, &ignore, depth);
match format {
CiFormat::Terminal => ci::render_terminal(&result),
CiFormat::Json => ci::render_json(&result)?,
}
let wh_cfg = webhook::load_config(&root);
let _ = webhook::fire_from_ci(&root, &wh_cfg, &result);
if result.passed() {
Ok(0)
} else {
Ok(1)
}
}
Commands::Search {
query: query_raw,
manifest: manifest_path,
output: output_path,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let m = manifest::Manifest::load(&path)?;
let total = m.services.len();
let q = search::Query::parse(&query_raw);
let matches = search::run(&m, &q);
if let Some(out_path) = output_path {
let content = search::render_json(&matches, &query_raw, total)?;
std::fs::write(&out_path, &content)?;
eprintln!("wrote search results to {}", out_path.display());
} else {
search::render(&matches, &query_raw, total);
}
Ok(0)
}
Commands::Deps {
manifest: manifest_path,
format,
output: output_path,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let m = manifest::Manifest::load(&path)?;
let dep_report = deps::analyze(&m);
if let Some(out_path) = output_path {
let content = deps::render_json_to_string(&dep_report)?;
std::fs::write(&out_path, &content)?;
eprintln!("wrote deps report to {}", out_path.display());
} else {
match format {
DepsFormat::Terminal => deps::render_terminal(&dep_report),
DepsFormat::Mermaid => deps::render_mermaid(&dep_report),
DepsFormat::Json => deps::render_json(&dep_report)?,
}
}
if dep_report.has_errors() {
Ok(1)
} else {
Ok(0)
}
}
Commands::Tag { action } => match action {
TagAction::Add {
service,
tag,
manifest: manifest_path,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
tag::add(&path, &service, &tag)?;
Ok(0)
}
TagAction::Remove {
service,
tag,
manifest: manifest_path,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
tag::remove(&path, &service, &tag)?;
Ok(0)
}
},
Commands::Scorecard {
manifest: manifest_path,
ignore: cli_ignore,
depth,
format,
output: output_path,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let m = manifest::Manifest::load(&path)?;
let mut ignore: Vec<String> = cfg.ignore.clone();
ignore.extend(cli_ignore);
let sc = scorecard::run(&m, &root, &ignore, depth);
match &output_path {
Some(out_path) => {
let content = match format {
ScorecardFormat::Terminal | ScorecardFormat::Markdown => {
scorecard::render_markdown(&sc)
}
ScorecardFormat::Json => scorecard::render_json_to_string(&sc)?,
};
std::fs::write(out_path, &content)?;
eprintln!("wrote scorecard to {}", out_path.display());
}
None => match format {
ScorecardFormat::Terminal => scorecard::render_terminal(&sc),
ScorecardFormat::Json => scorecard::render_json(&sc)?,
ScorecardFormat::Markdown => {
print!("{}", scorecard::render_markdown(&sc));
}
},
}
Ok(0)
}
Commands::Webhook {
manifest: manifest_path,
ignore: cli_ignore,
depth,
url: url_override,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let m = manifest::Manifest::load(&path)?;
let mut ignore: Vec<String> = cfg.ignore.clone();
ignore.extend(cli_ignore);
let discovered = discovery::discover_services_with_opts(&root, &m, &ignore, depth);
let drift_report = drift::analyze(&m, &discovered, &root);
let mut wh_cfg = webhook::load_config(&root);
if let Some(u) = url_override {
wh_cfg.url = Some(u);
wh_cfg.on_errors = true;
wh_cfg.on_warnings = true;
}
webhook::fire_from_drift(&root, &wh_cfg, &drift_report)?;
Ok(0)
}
Commands::Stats {
manifest: manifest_path,
} => {
let path = manifest_path.unwrap_or_else(|| manifest::find_default(&root));
let m = manifest::Manifest::load(&path)?;
stats::run(&m);
Ok(0)
}
Commands::Serve {
manifest: _manifest_path,
port,
refresh,
} => {
serve::serve(&root, port, refresh)?;
Ok(0)
}
Commands::Workspace { action } => {
match action {
WorkspaceAction::Check {
config: config_path,
filter: _filter,
format,
fail_on_drift,
ignore: cli_ignore,
depth,
output: output_path,
} => {
let (workspace_config, workspace_root) =
workspace::load_workspace_config(&config_path)?;
let mut ignore = cfg.ignore.clone();
ignore.extend(cli_ignore);
let report = workspace::analyze_workspace(
&workspace_config,
&workspace_root,
&ignore,
depth,
)?;
let content = match format {
svccat::cli::OutputFormat::Json => output::workspace::render_json(&report)?,
svccat::cli::OutputFormat::Markdown => {
output::workspace::render_markdown(&report)
}
_ => {
output::workspace::render_terminal(&report);
String::new()
}
};
if let Some(out_path) = output_path {
std::fs::write(&out_path, &content)?;
eprintln!("wrote workspace report to {}", out_path.display());
} else if !content.is_empty() {
print!("{}", content);
}
if fail_on_drift && report.has_errors() {
Ok(1)
} else {
Ok(0)
}
}
}
}
Commands::Demo { keep } => demo::run(keep),
Commands::Completions { shell } => {
let mut cmd = Cli::command();
generate(shell, &mut cmd, "svccat", &mut io::stdout());
Ok(0)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_report() -> drift::DriftReport {
serde_json::from_value(serde_json::json!({
"manifest": "services.yaml",
"declared": 2,
"discovered": 2,
"drifts": [
{
"kind": "declared_missing_from_repo",
"severity": "error",
"service": "api",
"message": "missing service directory",
"detail": null
}
]
}))
.unwrap()
}
#[test]
fn string_output_helper_supports_csv_slack_teams_and_datadog() {
let report = sample_report();
let csv = render_check_output_to_string(&OutputFormat::Csv, &report, &[])
.unwrap()
.unwrap();
assert!(csv.contains("service,severity,kind,message,detail"));
assert!(csv.contains("api,error,declared_missing_from_repo,missing service directory,"));
let slack = render_check_output_to_string(&OutputFormat::Slack, &report, &[])
.unwrap()
.unwrap();
assert!(slack.contains("blocks"));
let teams = render_check_output_to_string(&OutputFormat::Teams, &report, &[])
.unwrap()
.unwrap();
assert!(teams.contains("attachments"));
let datadog = render_check_output_to_string(&OutputFormat::Datadog, &report, &[])
.unwrap()
.unwrap();
assert!(datadog.contains("events"));
}
#[test]
fn string_output_helper_skips_terminal_format() {
let report = sample_report();
let out = render_check_output_to_string(&OutputFormat::Terminal, &report, &[]).unwrap();
assert!(out.is_none());
}
}