use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(name = "tsoracle", version, about = "Standalone timestamp oracle")]
pub struct Cli {
#[command(subcommand)]
pub cmd: Option<Cmd>,
#[command(flatten)]
pub serve_file: FileArgs,
}
#[derive(Subcommand, Debug)]
pub enum Cmd {
#[command(subcommand)]
Serve(Box<ServeCmd>),
Init(InitArgs),
#[cfg(feature = "openraft")]
#[command(subcommand)]
Admin(AdminCmd),
}
#[cfg(feature = "openraft")]
#[derive(Subcommand, Debug)]
pub enum AdminCmd {
Members(MembersArgs),
AddLearner(AddLearnerArgs),
Promote(AdminIdArgs),
Remove(AdminIdArgs),
ActivateFormat(ActivateFormatArgs),
Capabilities(CapabilitiesArgs),
}
#[cfg(feature = "openraft")]
#[derive(Parser, Debug, Clone)]
pub struct AdminClientTlsArgs {
#[arg(long)]
pub client_tls_cert: Option<std::path::PathBuf>,
#[arg(long)]
pub client_tls_key: Option<std::path::PathBuf>,
#[arg(long)]
pub client_tls_ca: Option<std::path::PathBuf>,
}
#[cfg(feature = "openraft")]
#[derive(Parser, Debug)]
pub struct MembersArgs {
#[arg(long)]
pub endpoint: String,
#[arg(long)]
pub capabilities: bool,
#[command(flatten)]
pub tls: AdminClientTlsArgs,
}
#[cfg(feature = "openraft")]
#[derive(Parser, Debug)]
pub struct CapabilitiesArgs {
#[arg(long)]
pub endpoint: String,
#[arg(long)]
pub json: bool,
#[command(flatten)]
pub tls: AdminClientTlsArgs,
}
#[cfg(feature = "openraft")]
#[derive(Parser, Debug)]
pub struct AdminIdArgs {
#[arg(long)]
pub endpoint: String,
#[arg(long)]
pub id: u64,
#[command(flatten)]
pub tls: AdminClientTlsArgs,
}
#[cfg(feature = "openraft")]
#[derive(Parser, Debug)]
pub struct AddLearnerArgs {
#[arg(long)]
pub endpoint: String,
#[arg(long)]
pub id: u64,
#[arg(long)]
pub raft_addr: String,
#[arg(long)]
pub service_endpoint: String,
#[arg(long)]
pub admin_endpoint: String,
#[command(flatten)]
pub tls: AdminClientTlsArgs,
}
#[cfg(feature = "openraft")]
#[derive(Parser, Debug)]
pub struct ActivateFormatArgs {
#[arg(long)]
pub endpoint: String,
#[arg(long)]
pub target: u8,
#[command(flatten)]
pub tls: AdminClientTlsArgs,
}
#[derive(Subcommand, Debug)]
pub enum ServeCmd {
File(FileArgs),
Openraft(OpenraftArgs),
Paxos(PaxosArgs),
}
#[derive(Parser, Debug, Clone)]
pub struct CommonServeArgs {
#[arg(long, default_value = "127.0.0.1:50551")]
pub listen: SocketAddr,
#[arg(long, value_parser = parse_duration, default_value = "3s")]
pub window_ahead: Duration,
#[arg(long, value_parser = parse_duration, default_value = "1s")]
pub failover_advance: Duration,
#[arg(long, value_parser = parse_duration, default_value = "10s")]
pub heartbeat_interval: Duration,
#[arg(long, default_value = "info")]
pub log: String,
#[cfg(feature = "metrics")]
#[arg(long, default_value = "127.0.0.1:9551")]
pub metrics_listen: SocketAddr,
#[arg(long)]
pub no_metrics: bool,
#[arg(long)]
pub tls_cert: Option<std::path::PathBuf>,
#[arg(long)]
pub tls_key: Option<std::path::PathBuf>,
#[arg(long)]
pub tls_client_ca: Option<std::path::PathBuf>,
}
#[derive(Parser, Debug, Clone)]
pub struct FileArgs {
#[command(flatten)]
pub common: CommonServeArgs,
#[arg(long, default_value = "./tsoracle-data")]
pub state_dir: PathBuf,
}
#[derive(Parser, Debug, Clone)]
pub struct OpenraftArgs {
#[command(flatten)]
pub common: CommonServeArgs,
#[arg(long)]
pub id: u64,
#[arg(long)]
pub raft_addr: SocketAddr,
#[arg(long)]
pub raft_dir: PathBuf,
#[arg(long)]
pub bootstrap: bool,
#[arg(long)]
pub members: Option<String>,
#[arg(long, default_value = "250")]
pub heartbeat_ms: u64,
#[arg(long, default_value = "1000")]
pub election_min_ms: u64,
#[arg(long, default_value = "2000")]
pub election_max_ms: u64,
#[arg(long)]
pub admin_listen: Option<SocketAddr>,
#[arg(long)]
pub admin_tls_cert: Option<std::path::PathBuf>,
#[arg(long)]
pub admin_tls_key: Option<std::path::PathBuf>,
#[arg(long)]
pub admin_tls_ca: Option<std::path::PathBuf>,
#[arg(long)]
pub peer_tls_cert: Option<std::path::PathBuf>,
#[arg(long)]
pub peer_tls_key: Option<std::path::PathBuf>,
#[arg(long)]
pub peer_tls_ca: Option<std::path::PathBuf>,
#[arg(long)]
pub allow_insecure_peer: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct PaxosArgs {
#[command(flatten)]
pub common: CommonServeArgs,
#[arg(long)]
pub node_id: u64,
#[arg(long)]
pub peer_listen: SocketAddr,
#[arg(long)]
pub peers: String,
#[arg(long)]
pub tso_peers: String,
#[arg(long)]
pub data_dir: PathBuf,
#[arg(long, value_parser = parse_duration, default_value = "20ms")]
pub tick_interval: Duration,
#[arg(long)]
pub peer_tls_cert: Option<std::path::PathBuf>,
#[arg(long)]
pub peer_tls_key: Option<std::path::PathBuf>,
#[arg(long)]
pub peer_tls_ca: Option<std::path::PathBuf>,
#[arg(long)]
pub allow_insecure_peer: bool,
}
#[derive(Parser, Debug)]
pub struct InitArgs {
#[arg(long, default_value = "./tsoracle-data")]
pub state_dir: PathBuf,
#[arg(long)]
pub seed_physical_ms: u64,
}
pub fn parse_duration(input: &str) -> Result<Duration, String> {
humantime::parse_duration(input).map_err(|e| e.to_string())
}
#[cfg(all(test, feature = "openraft"))]
mod admin_capabilities_parse_tests {
use super::{AdminCmd, Cli, Cmd};
use clap::Parser;
#[test]
fn capabilities_parses_endpoint() {
let cli = Cli::try_parse_from([
"tsoracle",
"admin",
"capabilities",
"--endpoint",
"http://127.0.0.1:51002",
])
.unwrap();
match cli.cmd {
Some(Cmd::Admin(AdminCmd::Capabilities(args))) => {
assert_eq!(args.endpoint, "http://127.0.0.1:51002");
assert!(!args.json);
}
other => panic!("expected Capabilities, got {other:?}"),
}
}
#[test]
fn capabilities_json_flag_sets_true() {
let cli = Cli::try_parse_from([
"tsoracle",
"admin",
"capabilities",
"--endpoint",
"http://x",
"--json",
])
.unwrap();
match cli.cmd {
Some(Cmd::Admin(AdminCmd::Capabilities(args))) => assert!(args.json),
other => panic!("expected Capabilities, got {other:?}"),
}
}
#[test]
fn members_capabilities_flag_sets_true() {
let cli = Cli::try_parse_from([
"tsoracle",
"admin",
"members",
"--endpoint",
"http://x",
"--capabilities",
])
.unwrap();
match cli.cmd {
Some(Cmd::Admin(AdminCmd::Members(args))) => {
assert_eq!(args.endpoint, "http://x");
assert!(args.capabilities);
}
other => panic!("expected Members, got {other:?}"),
}
}
}
#[cfg(all(test, feature = "metrics"))]
mod metrics_args_tests {
use super::{Cli, Cmd, ServeCmd};
use clap::Parser;
use std::net::SocketAddr;
#[test]
fn serve_metrics_args_default_to_loopback_exporter() {
let cli = Cli::try_parse_from([
"tsoracle",
"serve",
"file",
"--state-dir",
"/tmp/tsoracle-data",
])
.unwrap();
let Some(Cmd::Serve(serve)) = cli.cmd else {
panic!("expected serve command");
};
let ServeCmd::File(args) = *serve else {
panic!("expected file args");
};
assert_eq!(
args.common.metrics_listen,
SocketAddr::from(([127, 0, 0, 1], 9551))
);
assert!(!args.common.no_metrics);
}
#[test]
fn serve_metrics_args_allow_override_and_disable() {
let cli = Cli::try_parse_from([
"tsoracle",
"serve",
"file",
"--metrics-listen",
"0.0.0.0:9551",
"--no-metrics",
"--state-dir",
"/tmp/tsoracle-data",
])
.unwrap();
let Some(Cmd::Serve(serve)) = cli.cmd else {
panic!("expected serve command");
};
let ServeCmd::File(args) = *serve else {
panic!("expected file args");
};
assert_eq!(
args.common.metrics_listen,
SocketAddr::from(([0, 0, 0, 0], 9551))
);
assert!(args.common.no_metrics);
}
}
#[cfg(all(test, not(feature = "metrics")))]
mod no_metrics_flag_without_feature_tests {
use super::{Cli, Cmd, ServeCmd};
use clap::Parser;
#[test]
fn serve_accepts_no_metrics_without_feature() {
let cli = Cli::try_parse_from([
"tsoracle",
"serve",
"file",
"--no-metrics",
"--state-dir",
"/tmp/tsoracle-data",
])
.unwrap();
let Some(Cmd::Serve(serve)) = cli.cmd else {
panic!("expected serve command");
};
let ServeCmd::File(args) = *serve else {
panic!("expected file args");
};
assert!(args.common.no_metrics);
}
}