diff --git a/josh-proxy/src/bin/josh-proxy.rs b/josh-proxy/src/bin/josh-proxy.rs index 839fb271..c7cc7fb9 100644 --- a/josh-proxy/src/bin/josh-proxy.rs +++ b/josh-proxy/src/bin/josh-proxy.rs @@ -1,7 +1,10 @@ #![deny(warnings)] #[macro_use] extern crate lazy_static; +extern crate clap; +use clap::Parser; +use josh_proxy::cli; use josh_proxy::{run_git_with_auth, FetchError, MetaConfig, RemoteAuth, RepoConfig, RepoUpdate}; use opentelemetry::global; use opentelemetry::sdk::propagation::TraceContextPropagator; @@ -37,7 +40,7 @@ fn version_str() -> String { } lazy_static! { - static ref ARGS: josh_proxy::cli::Args = josh_proxy::cli::parse_args_or_exit(1); + static ref ARGS: josh_proxy::cli::Args = josh_proxy::cli::Args::parse(); } josh::regex_parsed!( @@ -1424,20 +1427,41 @@ fn trace_http_response_code(trace_span: Span, http_status: StatusCode) { }; } +/// Turn a list of [cli::Remote] into a [JoshProxyUpstream] struct. +fn make_upstream(remotes: &Vec) -> josh::JoshResult { + if remotes.is_empty() { + unreachable!() // already checked in the parser + } else if remotes.len() == 1 { + Ok(match &remotes[0] { + cli::Remote::Http(url) => JoshProxyUpstream::Http(url.to_string()), + cli::Remote::Ssh(url) => JoshProxyUpstream::Ssh(url.to_string()), + }) + } else if remotes.len() == 2 { + Ok(match (&remotes[0], &remotes[1]) { + (cli::Remote::Http(_), cli::Remote::Http(_)) + | (cli::Remote::Ssh(_), cli::Remote::Ssh(_)) => { + return Err(josh_error("two cli::remotes of the same type passed")) + } + (cli::Remote::Http(http_url), cli::Remote::Ssh(ssh_url)) + | (cli::Remote::Ssh(ssh_url), cli::Remote::Http(http_url)) => JoshProxyUpstream::Both { + http: http_url.to_string(), + ssh: ssh_url.to_string(), + }, + }) + } else { + Err(josh_error("too many remotes")) + } +} + #[tokio::main] async fn run_proxy() -> josh::JoshResult { let addr = format!("[::]:{}", ARGS.port).parse()?; - let upstream = match (&ARGS.remote.http, &ARGS.remote.ssh) { - (Some(http), None) => JoshProxyUpstream::Http(http.clone()), - (None, Some(ssh)) => JoshProxyUpstream::Ssh(ssh.clone()), - (Some(http), Some(ssh)) => JoshProxyUpstream::Both { - http: http.clone(), - ssh: ssh.clone(), - }, - (None, None) => return Err(josh_error("missing remote host url")), - }; + let upstream = make_upstream(&ARGS.remote).map_err(|e| { + eprintln!("Upstream parsing error: {}", &e); + e + })?; - let local = std::path::PathBuf::from(&ARGS.local); + let local = std::path::PathBuf::from(&ARGS.local.as_ref().unwrap()); let local = if local.is_absolute() { local } else { diff --git a/josh-proxy/src/cli.rs b/josh-proxy/src/cli.rs index 2aa1f7a6..19a8250b 100644 --- a/josh-proxy/src/cli.rs +++ b/josh-proxy/src/cli.rs @@ -1,175 +1,52 @@ -use josh::{josh_error, JoshResult}; +#[derive(Clone, Debug)] +pub enum Remote { + Http(String), + Ssh(String), +} -pub struct Remote { - pub http: Option, - pub ssh: Option, +fn parse_remote(s: &str) -> Result { + match s { + s if s.starts_with("http://") || s.starts_with("https://") => { + Ok(Remote::Http(s.to_string())) + } + s if s.starts_with("ssh://") => Ok(Remote::Ssh(s.to_string())), + _ => return Err("unsupported scheme"), + } } +#[derive(clap::Parser, Debug)] +#[command(name = "josh-proxy")] pub struct Args { - pub remote: Remote, - pub local: String, + #[arg(long, required = true, value_parser = parse_remote)] + pub remote: Vec, + #[arg(long, required = true)] + pub local: Option, + #[arg(name = "poll", long)] pub poll_user: Option, + #[arg(long, help = "Run git gc during maintenance")] pub gc: bool, + #[arg(long)] pub require_auth: bool, + #[arg(long)] pub no_background: bool, + + #[arg( + short, + help = "DEPRECATED - no effect! Number of concurrent upstream git fetch/push operations" + )] + _n: Option, + + #[arg(long, default_value = "8000")] pub port: u16, + #[arg( + short, + default_value = "0", + help = "Duration between forced cache refresh" + )] + #[arg(long, short)] pub cache_duration: u64, + #[arg(long, help = "Proxy static resource requests to a different URL")] pub static_resource_proxy_target: Option, + #[arg(long, help = "Filter to be prefixed to all queries of this instance")] pub filter_prefix: Option, } - -fn parse_int( - matches: &clap::ArgMatches, - arg_name: &str, - default: Option, -) -> JoshResult -where - ::Err: std::fmt::Display, -{ - let arg = matches.get_one::(arg_name).map(|s| s.as_str()); - - let arg = match (arg, default) { - (None, None) => { - return Err(josh_error(&format!( - "missing required argument: {}", - arg_name - ))) - } - (None, Some(default)) => Ok(default), - (Some(value), _) => value.parse::(), - }; - - arg.map_err(|e| josh_error(&format!("error parsing argument {}: {}", arg_name, e))) -} - -fn make_command() -> clap::Command { - clap::Command::new("josh-proxy") - .arg( - clap::Arg::new("remote") - .long("remote") - .action(clap::ArgAction::Append), - ) - .arg(clap::Arg::new("local").long("local")) - .arg(clap::Arg::new("poll").long("poll")) - .arg( - clap::Arg::new("gc") - .long("gc") - .action(clap::ArgAction::SetTrue) - .help("Run git gc during maintenance"), - ) - .arg( - clap::Arg::new("require-auth") - .long("require-auth") - .action(clap::ArgAction::SetTrue), - ) - .arg( - clap::Arg::new("no-background") - .long("no-background") - .action(clap::ArgAction::SetTrue), - ) - .arg(clap::Arg::new("n").short('n').help( - "DEPRECATED - no effect! Number of concurrent upstream git fetch/push operations", - )) - .arg(clap::Arg::new("port").long("port")) - .arg( - clap::Arg::new("cache-duration") - .long("cache-duration") - .short('c') - .help("Duration between forced cache refresh"), - ) - .arg( - clap::Arg::new("static-resource-proxy-target") - .long("static-resource-proxy-target") - .help("Proxy static resource requests to a different URL"), - ) - .arg( - clap::Arg::new("filter-prefix") - .long("filter-prefix") - .help("Filter to be prefixed to all queries of this instance"), - ) -} - -fn parse_remotes(values: &[String]) -> JoshResult { - let mut result = Remote { - http: None, - ssh: None, - }; - - for value in values { - match value { - v if v.starts_with("http://") || v.starts_with("https://") => { - result.http = match result.http { - None => Some(v.clone()), - Some(v) => return Err(josh_error(&format!("HTTP remote already set: {}", v))), - }; - } - v if v.starts_with("ssh://") => { - result.ssh = match result.ssh { - None => Some(v.clone()), - Some(v) => return Err(josh_error(&format!("SSH remote already set: {}", v))), - }; - } - _ => { - return Err(josh_error(&format!( - "Unsupported remote protocol: {}", - value - ))) - } - } - } - - Ok(result) -} - -pub fn parse_args() -> josh::JoshResult { - let args = make_command().get_matches_from(std::env::args()); - - let remote = args - .get_many::("remote") - .ok_or(josh_error("no remote specified"))? - .cloned() - .collect::>(); - let remote = parse_remotes(&remote)?; - - let local = args - .get_one::("local") - .ok_or(josh_error("missing local directory"))? - .clone(); - - let poll_user = args.get_one::("poll").map(String::clone); - let port = parse_int::(&args, "port", Some(8000))?; - let cache_duration = parse_int::(&args, "cache-duration", Some(0))?; - let static_resource_proxy_target = args - .get_one::("static-resource-proxy-target") - .map(String::clone); - - let filter_prefix = args.get_one::("filter-prefix").map(String::clone); - - Ok(Args { - remote, - local, - poll_user, - gc: args.get_flag("gc"), - require_auth: args.get_flag("require-auth"), - no_background: args.get_flag("no-background"), - port, - cache_duration, - static_resource_proxy_target, - filter_prefix, - }) -} - -pub fn parse_args_or_exit(code: i32) -> Args { - match parse_args() { - Err(e) => { - eprintln!("Argument parsing error: {}", e.0); - std::process::exit(code); - } - Ok(args) => args, - } -} - -#[test] -fn verify_app() { - make_command().debug_assert(); -} diff --git a/tests/proxy/shell.t b/tests/proxy/shell.t index 78216ab4..aec07460 100644 --- a/tests/proxy/shell.t +++ b/tests/proxy/shell.t @@ -1,12 +1,12 @@ $ . ${TESTDIR}/setup_test_env.sh $ kill -9 $(cat ${TESTTMP}/proxy_pid) $ ${TARGET_DIR}/debug/josh-proxy --help - Usage: josh-proxy [OPTIONS] + Usage: josh-proxy [OPTIONS] --remote --local Options: - --remote + --remote - --local + --local --poll @@ -16,15 +16,15 @@ --no-background - -n + -n DEPRECATED - no effect! Number of concurrent upstream git fetch/push operations - --port - - -c, --cache-duration - Duration between forced cache refresh - --static-resource-proxy-target + --port + [default: 8000] + -c, --cache-duration + Duration between forced cache refresh [default: 0] + --static-resource-proxy-target Proxy static resource requests to a different URL - --filter-prefix + --filter-prefix Filter to be prefixed to all queries of this instance -h, --help Print help