use std::net::SocketAddr;
use clap::{Parser, Subcommand};
use miette::Result;
use serde_json::Value;
use super::names::{ask, joined, list, tell, text};
use crate::actions::Context;
#[derive(Debug, Clone, Parser)]
#[clap(verbatim_doc_comment)]
pub struct CertsArgs {
#[command(subcommand)]
command: Option<Command>,
#[arg(long, global = true)]
server_addr: Vec<SocketAddr>,
#[arg(long, global = true)]
json: bool,
}
#[derive(Debug, Clone, Subcommand)]
enum Command {
List,
Request {
name: String,
},
Collect,
}
pub async fn run(args: CertsArgs, _ctx: Context) -> Result<()> {
let (endpoint, query) = match args.command.clone().unwrap_or(Command::List) {
Command::List => ("status", Vec::new()),
Command::Collect => ("collect", Vec::new()),
Command::Request { name } => ("request", vec![("name", name)]),
};
let answer = if endpoint == "status" {
ask(&args.server_addr, endpoint, &query).await?
} else {
tell(&args.server_addr, endpoint, &query).await?
};
if args.json {
println!("{}", serde_json::to_string_pretty(&answer).unwrap_or_default());
return Ok(());
}
report(&answer);
Ok(())
}
fn report(answer: &Value) {
let entitled = answer["entitled"].as_bool().unwrap_or(false);
let paused = answer["paused"].as_bool().unwrap_or(false);
println!(
"TLS grant: {}{}",
if entitled { "held" } else { "not held" },
if paused { " (canopy reports this server paused)" } else { "" },
);
println!("Domains: {}", joined(answer, "domains"));
let held: Vec<String> = answer["held"]
.as_array()
.map(|rows| {
rows.iter()
.map(|row| {
format!(
"{} — expires {}{}",
text(row, "name"),
text(row, "notAfter"),
if row["usable"].as_bool().unwrap_or(false) {
""
} else {
" (not servable)"
},
)
})
.collect()
})
.unwrap_or_default();
list("Chains this host serves", &held);
let canopy: Vec<String> = answer["canopyHolds"]
.as_array()
.map(|rows| {
rows.iter()
.map(|row| {
let mut line = format!("{} — expires {}", text(row, "name"), text(row, "notAfter"));
if row["revoked"].as_bool().unwrap_or(false) {
line.push_str(" (revoked)");
}
if row["keyMustBeReplaced"].as_bool().unwrap_or(false) {
line.push_str(" (key must be replaced)");
}
line
})
.collect()
})
.unwrap_or_default();
list("Certificates canopy holds", &canopy);
let orders: Vec<String> = answer["orders"]
.as_array()
.map(|rows| {
rows.iter()
.filter(|row| {
row["state"].as_str() != Some("issued") || !row["lastError"].is_null()
})
.map(|row| {
let mut line = format!("{} — {}", text(row, "name"), text(row, "state"));
if let Some(err) = row["lastError"].as_str() {
line.push_str(&format!(": {err}"));
}
line
})
.collect()
})
.unwrap_or_default();
if !orders.is_empty() {
list("Orders in flight", &orders);
}
if let Some(err) = answer["lastError"].as_str() {
println!("Last pass failed: {err}");
}
}