#![forbid(unsafe_code)]
use std::net::SocketAddr;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(version, about = "The Pulsate application gateway")]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Debug, Subcommand)]
enum Command {
Up {
#[arg(default_value = "pulsate.flow")]
config: PathBuf,
#[arg(long, default_value = "127.0.0.1:8080")]
listen: SocketAddr,
#[arg(long)]
tls_listen: Option<SocketAddr>,
#[arg(long)]
cert: Option<PathBuf>,
#[arg(long)]
key: Option<PathBuf>,
#[arg(long, default_value = "127.0.0.1:9100")]
metrics: String,
#[arg(long, default_value = "127.0.0.1:9180")]
admin: String,
#[arg(long)]
admin_token: Option<String>,
#[arg(long)]
http3_port: Option<u16>,
},
Plugin {
#[command(subcommand)]
action: PluginAction,
},
Import {
format: String,
file: PathBuf,
},
Validate {
#[arg(default_value = "pulsate.flow")]
config: PathBuf,
},
Config {
#[command(subcommand)]
action: ConfigAction,
},
Info,
}
#[derive(Debug, Subcommand)]
enum ConfigAction {
Dump {
#[arg(default_value = "pulsate.flow")]
config: PathBuf,
},
}
#[derive(Debug, Subcommand)]
enum PluginAction {
Run {
file: PathBuf,
#[arg(default_value_t = 1)]
input: i32,
},
}
#[must_use]
pub fn run() -> ExitCode {
let cli = Cli::parse();
match cli.command.unwrap_or(Command::Info) {
Command::Info => {
print_info();
ExitCode::SUCCESS
}
Command::Validate { config } => emit(&pulsate_cli::validate(&config)),
Command::Config {
action: ConfigAction::Dump { config },
} => emit(&pulsate_cli::config_dump(&config)),
Command::Up {
config,
listen,
tls_listen,
cert,
key,
metrics,
admin,
admin_token,
http3_port,
} => run_up(RunUp {
config,
listen,
tls_listen,
cert,
key,
metrics,
admin,
admin_token,
http3_port,
}),
Command::Plugin {
action: PluginAction::Run { file, input },
} => emit(&pulsate_cli::plugin_run(&file, input)),
Command::Import { format, file } => emit(&pulsate_cli::import_config(&format, &file)),
}
}
struct RunUp {
config: PathBuf,
listen: SocketAddr,
tls_listen: Option<SocketAddr>,
cert: Option<PathBuf>,
key: Option<PathBuf>,
metrics: String,
admin: String,
admin_token: Option<String>,
http3_port: Option<u16>,
}
fn run_up(args: RunUp) -> ExitCode {
let RunUp {
config,
listen,
tls_listen,
cert,
key,
metrics,
admin,
admin_token,
http3_port,
} = args;
let tls = match (tls_listen, cert, key) {
(None, None, None) => None,
(Some(listen), Some(cert), Some(key)) => {
Some(pulsate_cli::TlsOptions { listen, cert, key })
}
_ => {
eprintln!("pulsate: --tls-listen, --cert, and --key must be provided together");
return ExitCode::from(64); }
};
let metrics = match parse_optional_addr(&metrics, "--metrics") {
Ok(a) => a,
Err(code) => return code,
};
let admin = match parse_optional_addr(&admin, "--admin") {
Ok(a) => a,
Err(code) => return code,
};
let rt = match pulsate_rt::Runtime::new(None) {
Ok(rt) => rt,
Err(e) => {
eprintln!("pulsate: failed to start runtime: {e}");
return ExitCode::from(1);
}
};
let code = rt.block_on(pulsate_cli::up(pulsate_cli::UpOptions {
config,
listen,
tls,
metrics,
admin,
admin_token,
http3_port,
}));
ExitCode::from(code)
}
fn parse_optional_addr(value: &str, flag: &str) -> Result<Option<SocketAddr>, ExitCode> {
if value.eq_ignore_ascii_case("off") {
return Ok(None);
}
match value.parse::<SocketAddr>() {
Ok(addr) => Ok(Some(addr)),
Err(e) => {
eprintln!("pulsate: invalid {flag} address {value:?}: {e}");
Err(ExitCode::from(64))
}
}
}
fn emit(outcome: &pulsate_cli::Outcome) -> ExitCode {
if !outcome.stdout.is_empty() {
print!("{}", outcome.stdout);
}
if !outcome.stderr.is_empty() {
eprint!("{}", outcome.stderr);
}
ExitCode::from(outcome.code)
}
fn print_info() {
let bin = invoked_name();
let version = env!("CARGO_PKG_VERSION");
println!("{bin} {version} — one binary, one config, one command");
println!();
println!(" {bin} up <config> serve a gateway");
println!(" {bin} validate <config> check a config without starting");
println!(" {bin} import <fmt> <file> translate nginx/caddy/haproxy/apache to Flow");
println!(" {bin} plugin run <file> run a WASM plugin");
}
fn invoked_name() -> String {
std::env::args()
.next()
.as_deref()
.map(std::path::Path::new)
.and_then(std::path::Path::file_stem)
.map_or_else(
|| "pulsate".to_string(),
|s| s.to_string_lossy().into_owned(),
)
}