use std::path::PathBuf;
use clap::{ArgGroup, Args, Parser, Subcommand};
#[derive(Subcommand, Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum EdgeMode {
Edge(EdgeArgs),
Offline(OfflineArgs),
}
#[derive(Args, Debug, Clone)]
pub struct ClientIdentity {
#[clap(long, env)]
pub pkcs8_client_certificate_file: Option<PathBuf>,
#[clap(long, env)]
pub pkcs8_client_key_file: Option<PathBuf>,
#[clap(long, env)]
pub pkcs12_identity_file: Option<PathBuf>,
#[clap(long, env)]
pub pkcs12_passphrase: Option<String>,
}
#[derive(Args, Debug, Clone)]
#[command(group(
ArgGroup::new("data-provider")
.args(["redis_url", "backup_folder"]),
))]
pub struct EdgeArgs {
#[clap(short, long, env)]
pub upstream_url: String,
#[clap(short, long, env)]
pub redis_url: Option<String>,
#[clap(short, long, env)]
pub backup_folder: Option<PathBuf>,
#[clap(short, long, env, default_value_t = 60)]
pub metrics_interval_seconds: u64,
#[clap(short, long, env, default_value_t = 10)]
pub features_refresh_interval_seconds: u64,
#[clap(long, env, default_value_t = 3600)]
pub token_revalidation_interval_seconds: u64,
#[clap(short, long, env, value_delimiter = ',')]
pub tokens: Vec<String>,
#[clap(short = 'H', long, env, value_delimiter = ',', value_parser = string_to_header_tuple)]
pub custom_client_headers: Vec<(String, String)>,
#[clap(short, long, env, default_value_t = false)]
pub skip_ssl_verification: bool,
#[clap(flatten)]
pub client_identity: Option<ClientIdentity>,
#[clap(long, env)]
pub upstream_certificate_file: Option<PathBuf>,
}
pub fn string_to_header_tuple(s: &str) -> Result<(String, String), String> {
let format_message = "Please pass headers in the format <headername>:<headervalue>".to_string();
if s.contains(':') {
if let Some((header_name, header_value)) = s.split_once(':') {
Ok((
header_name.trim().to_string(),
header_value.trim().to_string(),
))
} else {
Err(format_message)
}
} else {
Err(format_message)
}
}
#[derive(Args, Debug, Clone)]
pub struct OfflineArgs {
#[clap(short, long, env)]
pub bootstrap_file: Option<PathBuf>,
#[clap(short, long, env, value_delimiter = ',')]
pub tokens: Vec<String>,
}
#[derive(Parser, Debug, Clone)]
pub struct CliArgs {
#[clap(flatten)]
pub http: HttpServerArgs,
#[command(subcommand)]
pub mode: EdgeMode,
#[clap(long, env, default_value_t = ulid::Ulid::new().to_string())]
pub instance_id: String,
#[clap(short, long, env, default_value = "unleash-edge")]
pub app_name: String,
}
#[derive(Args, Debug, Clone)]
pub struct TlsOptions {
#[clap(env, long, default_value_t = false)]
pub tls_enable: bool,
#[clap(env, long)]
pub tls_server_key: Option<PathBuf>,
#[clap(env, long)]
pub tls_server_cert: Option<PathBuf>,
#[clap(env, long, default_value_t = 3043)]
pub tls_server_port: u16,
}
#[derive(Args, Debug, Clone)]
pub struct HttpServerArgs {
#[clap(short, long, env, default_value_t = 3063)]
pub port: u16,
#[clap(short, long, env, default_value = "0.0.0.0")]
pub interface: String,
#[clap(short, long, env, default_value_t = num_cpus::get_physical())]
pub workers: usize,
#[clap(flatten)]
pub tls: TlsOptions,
}
impl HttpServerArgs {
pub fn http_server_tuple(&self) -> (String, u16) {
(self.interface.clone(), self.port)
}
pub fn https_server_tuple(&self) -> (String, u16) {
(self.interface.clone(), self.tls.tls_server_port)
}
}
#[cfg(test)]
mod tests {
use crate::cli::{CliArgs, EdgeMode};
use clap::Parser;
#[test]
pub fn can_parse_multiple_client_headers() {
let args = vec![
"unleash-edge",
"edge",
"-u http://localhost:4242",
r#"-H Authorization: abc123"#,
r#"-H X-Api-Key: mysecret"#,
];
let args = CliArgs::parse_from(args);
match args.mode {
EdgeMode::Edge(args) => {
let client_headers = args.custom_client_headers;
assert_eq!(client_headers.len(), 2);
let auth = client_headers.get(0).unwrap();
assert_eq!(auth.0, "Authorization");
assert_eq!(auth.1, "abc123");
let api_key = client_headers.get(1).unwrap();
assert_eq!(api_key.0, "X-Api-Key");
assert_eq!(api_key.1, "mysecret")
}
EdgeMode::Offline(_) => unreachable!(),
}
}
#[test]
pub fn can_parse_comma_separated_client_headers() {
let args = vec![
"unleash-edge",
"edge",
"-u http://localhost:4242",
r#"-H Authorization: abc123,X-Api-Key: mysecret"#,
];
let args = CliArgs::parse_from(args);
match args.mode {
EdgeMode::Edge(args) => {
let client_headers = args.custom_client_headers;
assert_eq!(client_headers.len(), 2);
let auth = client_headers.get(0).unwrap();
assert_eq!(auth.0, "Authorization");
assert_eq!(auth.1, "abc123");
let api_key = client_headers.get(1).unwrap();
assert_eq!(api_key.0, "X-Api-Key");
assert_eq!(api_key.1, "mysecret")
}
EdgeMode::Offline(_) => unreachable!(),
}
}
#[test]
pub fn can_handle_colons_in_header_value() {
let args = vec![
"unleash-edge",
"edge",
"-u http://localhost:4242",
r#"-H Authorization: test:test.secret"#,
];
let args = CliArgs::parse_from(args);
match args.mode {
EdgeMode::Edge(args) => {
let client_headers = args.custom_client_headers;
assert_eq!(client_headers.len(), 1);
let auth = client_headers.get(0).unwrap();
assert_eq!(auth.0, "Authorization");
assert_eq!(auth.1, "test:test.secret");
}
EdgeMode::Offline(_) => unreachable!(),
}
}
}