use std::io::IsTerminal;
use std::path::PathBuf;
use std::process::ExitCode;
use aa_gateway::policy::history::{FsHistoryStore, HistoryConfig, PolicyHistoryStore};
use clap::Args;
use owo_colors::OwoColorize;
use serde::{Deserialize, Serialize};
use crate::config::ResolvedContext;
#[derive(Debug, Serialize)]
pub struct CreatePolicyRequest {
pub policy_yaml: String,
}
#[derive(Debug, Deserialize)]
pub struct PolicyApplyResponse {
pub name: String,
pub version: String,
pub active: bool,
pub rule_count: usize,
}
#[derive(Args)]
pub struct ApplyArgs {
pub file: PathBuf,
#[arg(long)]
pub applied_by: Option<String>,
}
#[derive(Args)]
pub struct HistoryArgs {
#[arg(short = 'n', long, default_value_t = 10)]
pub limit: usize,
}
#[derive(Args)]
pub struct RollbackArgs {
pub version: String,
}
#[derive(Args)]
pub struct DiffArgs {
pub version_a: String,
pub version_b: String,
}
pub fn run_apply(args: ApplyArgs, ctx: &ResolvedContext) -> ExitCode {
let rt = tokio::runtime::Runtime::new().expect("failed to create tokio runtime");
rt.block_on(async {
let yaml = match std::fs::read_to_string(&args.file) {
Ok(y) => y,
Err(e) => {
eprintln!("error: failed to read {}: {}", args.file.display(), e);
return ExitCode::FAILURE;
}
};
if let Err(errs) = aa_gateway::policy::PolicyValidator::from_yaml(&yaml) {
eprintln!("error: policy validation failed: {:?}", errs);
return ExitCode::FAILURE;
}
let body = CreatePolicyRequest { policy_yaml: yaml };
match crate::client::post_json::<_, PolicyApplyResponse>(ctx, "/api/v1/policies", &body).await {
Ok(resp) => {
println!("Policy applied successfully.");
println!(" Version: {}", resp.name);
println!(" Timestamp: {}", resp.version);
println!(" Active: {}", resp.active);
println!(" Rules: {}", resp.rule_count);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {}", e);
ExitCode::FAILURE
}
}
})
}
pub fn run_history(args: HistoryArgs) -> ExitCode {
let rt = tokio::runtime::Runtime::new().expect("failed to create tokio runtime");
rt.block_on(async {
let store = FsHistoryStore::new(HistoryConfig::default_config());
match store.list(args.limit).await {
Ok(versions) => {
if versions.is_empty() {
println!("No policy versions found.");
return ExitCode::SUCCESS;
}
println!(
"{:<14} {:<26} {:<12} {:<10} {:<16}",
"VERSION", "TIMESTAMP", "APPLIED BY", "ROLLBACK", "FIRST EVENT"
);
println!("{}", "-".repeat(80));
for meta in versions {
let version_short = &meta.sha256[..meta.sha256.len().min(12)];
let applied_by = meta.applied_by.as_deref().unwrap_or("-");
let rollback = if meta.is_rollback { "yes" } else { "-" };
let first_event = meta.first_event_covered.as_deref().unwrap_or("-");
println!(
"{:<14} {:<26} {:<12} {:<10} {:<16}",
version_short, meta.timestamp, applied_by, rollback, first_event
);
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {}", e);
ExitCode::FAILURE
}
}
})
}
pub fn run_rollback(args: RollbackArgs) -> ExitCode {
let rt = tokio::runtime::Runtime::new().expect("failed to create tokio runtime");
rt.block_on(async {
let store = FsHistoryStore::new(HistoryConfig::default_config());
match store.rollback(&args.version).await {
Ok(meta) => {
println!("Rolled back successfully.");
println!(" New version: {}", &meta.sha256[..12]);
println!(" Timestamp: {}", meta.timestamp);
println!(
" Rolled back to: {}",
meta.rollback_target.as_deref().unwrap_or("unknown")
);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {}", e);
ExitCode::FAILURE
}
}
})
}
pub fn run_diff(args: DiffArgs) -> ExitCode {
let rt = tokio::runtime::Runtime::new().expect("failed to create tokio runtime");
rt.block_on(async {
let store = FsHistoryStore::new(HistoryConfig::default_config());
match store.diff(&args.version_a, &args.version_b).await {
Ok(diff) => {
if diff.lines().count() <= 2 {
println!("No differences between the two versions.");
} else {
print_colored_diff(&diff);
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {}", e);
ExitCode::FAILURE
}
}
})
}
fn print_colored_diff(diff: &str) {
let use_color = std::io::stdout().is_terminal();
for line in diff.lines() {
if use_color {
println!("{}", colorize_diff_line(line));
} else {
println!("{line}");
}
}
}
fn colorize_diff_line(line: &str) -> String {
if line.starts_with("---") {
line.red().to_string()
} else if line.starts_with("+++") {
line.green().to_string()
} else if line.starts_with("@@") {
line.cyan().to_string()
} else if line.starts_with('-') {
line.red().to_string()
} else if line.starts_with('+') {
line.green().to_string()
} else {
line.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn colorize_removal_header() {
let out = colorize_diff_line("--- abc123def456");
assert!(out.contains("--- abc123def456"));
assert!(out.contains("\x1b["));
}
#[test]
fn colorize_addition_header() {
let out = colorize_diff_line("+++ 789abc012def");
assert!(out.contains("+++ 789abc012def"));
assert!(out.contains("\x1b["));
}
#[test]
fn colorize_hunk_marker() {
let out = colorize_diff_line("@@ -1,3 +1,3 @@");
assert!(out.contains("@@ -1,3 +1,3 @@"));
assert!(out.contains("\x1b["));
}
#[test]
fn colorize_removed_line() {
let out = colorize_diff_line("-max_actions_per_minute: 100");
assert!(out.contains("-max_actions_per_minute: 100"));
assert!(out.contains("\x1b["));
}
#[test]
fn colorize_added_line() {
let out = colorize_diff_line("+max_actions_per_minute: 200");
assert!(out.contains("+max_actions_per_minute: 200"));
assert!(out.contains("\x1b["));
}
#[test]
fn colorize_context_line_unchanged() {
let out = colorize_diff_line(" tier: low");
assert_eq!(out, " tier: low");
assert!(!out.contains("\x1b["));
}
#[test]
fn colorize_empty_line() {
let out = colorize_diff_line("");
assert_eq!(out, "");
}
#[test]
fn print_colored_diff_does_not_panic_on_empty() {
print_colored_diff("");
}
}