diff --git a/CLI.md b/CLI.md new file mode 100644 index 00000000..5d0bb365 --- /dev/null +++ b/CLI.md @@ -0,0 +1,120 @@ +# Command-Line Help for `unleash-edge` + +This document contains the help content for the `unleash-edge` command-line program. + +**Command Overview:** + +* [`unleash-edge`↴](#unleash-edge) +* [`unleash-edge edge`↴](#unleash-edge-edge) +* [`unleash-edge offline`↴](#unleash-edge-offline) + +## `unleash-edge` + +**Usage:** `unleash-edge [OPTIONS] ` + +###### **Subcommands:** + +* `edge` — Run in edge mode +* `offline` — Run in offline mode + +###### **Options:** + +* `-p`, `--port ` — Which port should this server listen for HTTP traffic on + + Default value: `3063` +* `-i`, `--interface ` — Which interfaces should this server listen for HTTP traffic on + + Default value: `0.0.0.0` +* `-b`, `--base-path ` — Which base path should this server listen for HTTP traffic on + + Default value: `` +* `-w`, `--workers ` — How many workers should be started to handle requests. Defaults to number of physical cpus + + Default value: `16` +* `--enable-post-features` — Exposes the api/client/features endpoint for POST requests. This may be removed in a future release + + Default value: `false` +* `--tls-enable` — Should we bind TLS + + Default value: `false` +* `--tls-server-key ` — Server key to use for TLS +* `--tls-server-cert ` — Server Cert to use for TLS +* `--tls-server-port ` — Port to listen for https connection on (will use the interfaces already defined) + + Default value: `3043` +* `--instance-id ` — Instance id. Used for metrics reporting + + Default value: `01H25S347DVN3YGHBYTV8GBRFM` +* `-a`, `--app-name ` — App name. Used for metrics reporting + + Default value: `unleash-edge` +* `--markdown-help` + + + +## `unleash-edge edge` + +Run in edge mode + +**Usage:** `unleash-edge edge [OPTIONS] --upstream-url ` + +###### **Options:** + +* `-u`, `--upstream-url ` — Where is your upstream URL. Remember, this is the URL to your instance, without any trailing /api suffix +* `--redis-url ` +* `--redis-password ` +* `--redis-username ` +* `--redis-port ` +* `--redis-host ` +* `--redis-secure` + + Default value: `false` +* `--redis-scheme ` + + Default value: `redis` + + Possible values: `redis`, `rediss`, `redis-unix`, `unix` + +* `-b`, `--backup-folder ` — A path to a local folder. Edge will write feature and token data to disk in this folder and read this back after restart. Mutually exclusive with the --redis-url option +* `-m`, `--metrics-interval-seconds ` — How often should we post metrics upstream? + + Default value: `60` +* `-f`, `--features-refresh-interval-seconds ` — How long between each refresh for a token + + Default value: `10` +* `--token-revalidation-interval-seconds ` — How long between each revalidation of a token + + Default value: `3600` +* `-t`, `--tokens ` — Get data for these client tokens at startup. Hot starts your feature cache +* `-H`, `--custom-client-headers ` — Expects curl header format (-H : ) for instance `-H X-Api-Key: mysecretapikey` +* `-s`, `--skip-ssl-verification` — If set to true, we will skip SSL verification when connecting to the upstream Unleash server + + Default value: `false` +* `--pkcs8-client-certificate-file ` — Client certificate chain in PEM encoded X509 format with the leaf certificate first. The certificate chain should contain any intermediate certificates that should be sent to clients to allow them to build a chain to a trusted root +* `--pkcs8-client-key-file ` — Client key is a PEM encoded PKCS#8 formatted private key for the leaf certificate +* `--pkcs12-identity-file ` — Identity file in pkcs12 format. Typically this file has a pfx extension +* `--pkcs12-passphrase ` — Passphrase used to unlock the pkcs12 file +* `--upstream-certificate-file ` — Extra certificate passed to the client for building its trust chain. Needs to be in PEM format (crt or pem extensions usually are) + + + +## `unleash-edge offline` + +Run in offline mode + +**Usage:** `unleash-edge offline [OPTIONS]` + +###### **Options:** + +* `-b`, `--bootstrap-file ` +* `-t`, `--tokens ` + + + +
+ + + This document was generated automatically by + clap-markdown. + + diff --git a/Cargo.lock b/Cargo.lock index 168f6ea4..81d62aa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -583,6 +583,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "clap-markdown" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325f50228f76921784b6d9f2d62de6778d834483248eefecd27279174797e579" +dependencies = [ + "clap 4.2.7", +] + [[package]] name = "clap_builder" version = "4.2.7" @@ -3030,6 +3039,7 @@ dependencies = [ "async-trait", "chrono", "clap 4.2.7", + "clap-markdown", "dashmap", "dotenv", "env_logger", diff --git a/server/Cargo.toml b/server/Cargo.toml index 21c6b107..9fa8a5d3 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -25,6 +25,7 @@ anyhow = "1.0.71" async-trait = "0.1.68" chrono = {version = "0.4.24", features = ["serde"]} clap = {version = "4.2.7", features = ["derive", "env"]} +clap-markdown = "0.1.3" dashmap = "5.4.0" dotenv = {version = "0.15.0", features = ["clap"]} futures = "0.3.28" diff --git a/server/src/builder.rs b/server/src/builder.rs index 607eda61..5b379b2e 100644 --- a/server/src/builder.rs +++ b/server/src/builder.rs @@ -110,7 +110,7 @@ fn build_offline(offline_args: OfflineArgs) -> EdgeResult { } async fn get_data_source(args: &EdgeArgs) -> Option> { - if let Some(redis_url) = args.redis_url.clone() { + if let Some(redis_url) = args.redis.clone().and_then(|r| r.to_url()) { let redis_client = RedisPersister::new(&redis_url).expect("Failed to connect to Redis"); return Some(Arc::new(redis_client)); } diff --git a/server/src/cli.rs b/server/src/cli.rs index b6b7aa0b..f55aa9c9 100644 --- a/server/src/cli.rs +++ b/server/src/cli.rs @@ -1,6 +1,7 @@ +use std::fmt::{Display, Formatter}; use std::path::PathBuf; -use clap::{ArgGroup, Args, Parser, Subcommand}; +use clap::{ArgGroup, Args, Parser, Subcommand, ValueEnum}; #[derive(Subcommand, Debug, Clone)] #[allow(clippy::large_enum_variant)] @@ -11,6 +12,71 @@ pub enum EdgeMode { Offline(OfflineArgs), } +#[derive(ValueEnum, Debug, Clone)] +pub enum RedisScheme { + Redis, + Rediss, + RedisUnix, + Unix, +} + +impl Display for RedisScheme { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RedisScheme::Redis => write!(f, "redis"), + RedisScheme::Rediss => write!(f, "rediss"), + RedisScheme::RedisUnix => write!(f, "redis+unix"), + RedisScheme::Unix => write!(f, "unix"), + } + } +} +#[derive(Args, Debug, Clone)] +pub struct RedisArgs { + #[clap(long, env)] + pub redis_url: Option, + #[clap(long, env)] + pub redis_password: Option, + #[clap(long, env)] + pub redis_username: Option, + #[clap(long, env)] + pub redis_port: Option, + #[clap(long, env)] + pub redis_host: Option, + #[clap(long, env, default_value_t = false)] + pub redis_secure: bool, + #[clap(long, env, default_value_t = RedisScheme::Redis, value_enum)] + pub redis_scheme: RedisScheme, +} + +impl RedisArgs { + pub fn to_url(&self) -> Option { + self.redis_url + .clone() + .map(|url| { + reqwest::Url::parse(&url).unwrap_or_else(|_| panic!("Failed to create url from REDIS_URL: {}, REDIS_USERNAME: {} and REDIS_PASSWORD: {}", self.redis_url.clone().unwrap_or("NO_URL".into()), self.redis_username.clone().unwrap_or("NO_USERNAME_SET".into()), self.redis_password.is_some())) + }) + .or_else(|| self.redis_host.clone().map(|host| { + reqwest::Url::parse(format!("{}://{}", self.redis_scheme, &host).as_str()).expect("Failed to parse hostname from REDIS_HOSTNAME or --redis-hostname parameters") + })) + .map(|base| { + let mut base_url = base; + if self.redis_password.is_some() { + base_url.set_password(Some(&self.redis_password.clone().unwrap())).expect("Failed to set password"); + } + if self.redis_username.is_some() { + base_url.set_username(&self.redis_username.clone().unwrap()).expect("Failed to set username"); + } + base_url.set_port(self.redis_port).expect("Failed to set port"); + base_url + }).map(|almost_finished_url| { + let mut base_url = almost_finished_url; + if self.redis_secure { + base_url.set_scheme("rediss").expect("Failed to set redis scheme"); + } + base_url + }).map(|f| f.to_string()) + } +} #[derive(Args, Debug, Clone)] pub struct ClientIdentity { /// Client certificate chain in PEM encoded X509 format with the leaf certificate first. @@ -39,8 +105,8 @@ pub struct EdgeArgs { pub upstream_url: String, /// A URL pointing to a running Redis instance. Edge will use this instance to persist feature and token data and read this back after restart. Mutually exclusive with the --backup-folder option - #[clap(short, long, env)] - pub redis_url: Option, + #[clap(flatten)] + pub redis: Option, /// A path to a local folder. Edge will write feature and token data to disk in this folder and read this back after restart. Mutually exclusive with the --redis-url option #[clap(short, long, env)] @@ -116,6 +182,9 @@ pub struct CliArgs { /// App name. Used for metrics reporting. #[clap(short, long, env, default_value = "unleash-edge")] pub app_name: String, + + #[arg(long, hide = true)] + pub markdown_help: bool, } #[derive(Args, Debug, Clone)] @@ -155,7 +224,7 @@ pub struct HttpServerArgs { /// How many workers should be started to handle requests. /// Defaults to number of physical cpus - #[clap(short, long, env, default_value_t = num_cpus::get_physical())] + #[clap(short, long, env, global=true, default_value_t = num_cpus::get_physical())] pub workers: usize, #[clap(flatten)] @@ -249,4 +318,128 @@ mod tests { EdgeMode::Offline(_) => unreachable!(), } } + + #[test] + pub fn can_create_redis_url_from_redis_url_argument() { + let args = vec![ + "unleash-edge", + "edge", + "-u http://localhost:4242", + "--redis-url", + "redis://localhost/redis", + ]; + let args = CliArgs::parse_from(args); + match args.mode { + EdgeMode::Edge(args) => { + let redis_url = args.redis.unwrap().to_url(); + assert!(redis_url.is_some()); + assert_eq!(redis_url.unwrap(), "redis://localhost/redis"); + } + _ => unreachable!(), + } + } + + #[test] + pub fn can_create_redis_url_from_more_specific_redis_arguments() { + let args = vec![ + "unleash-edge", + "edge", + "-u http://localhost:4242", + "--redis-host", + "localhost", + "--redis-username", + "redis", + "--redis-password", + "password", + "--redis-port", + "6389", + "--redis-scheme", + "rediss", + ]; + let args = CliArgs::parse_from(args); + match args.mode { + EdgeMode::Edge(args) => { + let redis_url = args.redis.unwrap().to_url(); + assert!(redis_url.is_some()); + assert_eq!(redis_url.unwrap(), "rediss://redis:password@localhost:6389"); + } + _ => unreachable!(), + } + } + + #[test] + pub fn can_combine_redis_url_with_username_and_password() { + let args = vec![ + "unleash-edge", + "edge", + "-u http://localhost:4242", + "--redis-url", + "redis://localhost", + "--redis-username", + "redis", + "--redis-password", + "password", + ]; + let args = CliArgs::parse_from(args); + match args.mode { + EdgeMode::Edge(args) => { + let redis_url = args.redis.unwrap().to_url(); + assert!(redis_url.is_some()); + assert_eq!(redis_url.unwrap(), "redis://redis:password@localhost"); + } + _ => unreachable!(), + } + } + + #[test] + pub fn setting_redis_secure_to_true_overrides_set_scheme() { + let args = vec![ + "unleash-edge", + "edge", + "-u http://localhost:4242", + "--redis-url", + "redis://localhost", + "--redis-username", + "redis", + "--redis-password", + "password", + "--redis-secure", + ]; + let args = CliArgs::parse_from(args); + match args.mode { + EdgeMode::Edge(args) => { + let redis_url = args.redis.unwrap().to_url(); + assert!(redis_url.is_some()); + assert_eq!(redis_url.unwrap(), "rediss://redis:password@localhost"); + } + _ => unreachable!(), + } + } + + #[test] + pub fn setting_secure_to_true_overrides_the_scheme_for_detailed_arguments_as_well() { + let args = vec![ + "unleash-edge", + "edge", + "-u http://localhost:4242", + "--redis-host", + "localhost", + "--redis-username", + "redis", + "--redis-password", + "password", + "--redis-port", + "6389", + "--redis-secure", + ]; + let args = CliArgs::parse_from(args); + match args.mode { + EdgeMode::Edge(args) => { + let redis_url = args.redis.unwrap().to_url(); + assert!(redis_url.is_some()); + assert_eq!(redis_url.unwrap(), "rediss://redis:password@localhost:6389"); + } + _ => unreachable!(), + } + } } diff --git a/server/src/main.rs b/server/src/main.rs index 023007ec..b1316b60 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -31,6 +31,10 @@ use utoipa_swagger_ui::SwaggerUi; async fn main() -> Result<(), anyhow::Error> { dotenv::dotenv().ok(); let args = CliArgs::parse(); + if args.markdown_help { + clap_markdown::print_help_markdown::(); + return Ok(()); + } let schedule_args = args.clone(); let mode_arg = args.clone().mode; let http_args = args.clone().http;