Skip to main content

relay/
cli.rs

1use std::path::{Path, PathBuf};
2use std::{env, io};
3
4use anyhow::{Result, anyhow, bail};
5use clap::ArgMatches;
6use clap_complete::Shell;
7use dialoguer::{Confirm, Select};
8use relay_config::{
9    Config, ConfigError, ConfigErrorKind, Credentials, MinimalConfig, OverridableConfig, RelayMode,
10};
11use uuid::Uuid;
12
13use crate::cliapp::make_app;
14use crate::healthcheck::healthcheck;
15use crate::utils::get_theme;
16use crate::{setup, utils};
17
18fn load_config(path: impl AsRef<Path>, require: bool) -> Result<Config> {
19    match Config::from_path(path) {
20        Ok(config) => Ok(config),
21        Err(error) => {
22            if let Some(config_error) = error.downcast_ref::<ConfigError>()
23                && !require
24                && config_error.kind() == ConfigErrorKind::CouldNotOpenFile
25            {
26                return Ok(Config::default());
27            }
28
29            Err(error)
30        }
31    }
32}
33
34/// Runs the command line application.
35pub fn execute() -> Result<()> {
36    let app = make_app();
37    let matches = app.get_matches();
38    let config_path = matches
39        .get_one::<PathBuf>("config")
40        .map_or(Path::new(".relay"), PathBuf::as_path);
41
42    // Commands that do not need to load the config:
43    if let Some(matches) = matches.subcommand_matches("config") {
44        if let Some(matches) = matches.subcommand_matches("init") {
45            return init_config(config_path, matches);
46        }
47    } else if let Some(matches) = matches.subcommand_matches("generate-completions") {
48        return generate_completions(matches);
49    }
50
51    // Commands that need a loaded config:
52    let mut config = load_config(config_path, matches.contains_id("config"))?;
53    // override file config with environment variables
54    let env_config = extract_config_env_vars();
55    config.apply_override(env_config)?;
56    // override config with global command line arguments
57    let global_config = extract_global_config_args(&matches);
58    config.apply_override(global_config)?;
59
60    // SAFETY: The function cannot be called from a multi threaded environment,
61    // this is the main entry point where no other threads have been spawned yet.
62    unsafe {
63        let config = config.current();
64        relay_log::init(config.logging(), config.sentry());
65    }
66
67    if let Some(matches) = matches.subcommand_matches("config") {
68        manage_config(&config, matches)
69    } else if let Some(matches) = matches.subcommand_matches("credentials") {
70        manage_credentials(config, matches)
71    } else if let Some(matches) = matches.subcommand_matches("healthcheck") {
72        healthcheck(&config, matches)
73    } else if let Some(matches) = matches.subcommand_matches("run") {
74        // override config with run command args
75        let arg_config = extract_config_args(matches);
76        config.apply_override(arg_config)?;
77        run(config, matches)
78    } else {
79        unreachable!();
80    }
81}
82
83/// Extract config arguments from the global command line arguments.
84pub fn extract_global_config_args(matches: &ArgMatches) -> OverridableConfig {
85    OverridableConfig {
86        log_level: matches.get_one("log_level").cloned(),
87        log_format: matches.get_one("log_format").cloned(),
88        ..Default::default()
89    }
90}
91
92/// Extract config arguments from a parsed command line arguments object.
93pub fn extract_config_args(matches: &ArgMatches) -> OverridableConfig {
94    let processing = if matches.get_flag("processing") {
95        Some("true".to_owned())
96    } else if matches.get_flag("no_processing") {
97        Some("false".to_owned())
98    } else {
99        None
100    };
101
102    OverridableConfig {
103        mode: matches.get_one("mode").cloned(),
104        log_level: None,
105        log_format: None,
106        upstream: matches.get_one("upstream").cloned(),
107        upstream_dsn: matches.get_one("upstream_dsn").cloned(),
108        host: matches.get_one("host").cloned(),
109        port: matches.get_one("port").cloned(),
110        processing,
111        kafka_url: matches.get_one("kafka_broker_url").cloned(),
112        redis_url: matches.get_one("redis_url").cloned(),
113        id: matches.get_one("id").cloned(),
114        public_key: matches.get_one("public_key").cloned(),
115        secret_key: matches.get_one("secret_key").cloned(),
116        outcome_source: matches.get_one("source_id").cloned(),
117        shutdown_timeout: matches.get_one("shutdown_timeout").cloned(),
118        instance: matches.get_one("instance").cloned(),
119        server_name: matches.get_one("server_name").cloned(),
120    }
121}
122
123/// Extract config arguments from environment variables
124pub fn extract_config_env_vars() -> OverridableConfig {
125    OverridableConfig {
126        mode: env::var("RELAY_MODE").ok(),
127        log_level: env::var("RELAY_LOG_LEVEL").ok(),
128        log_format: env::var("RELAY_LOG_FORMAT").ok(),
129        upstream: env::var("RELAY_UPSTREAM_URL").ok(),
130        upstream_dsn: env::var("RELAY_UPSTREAM_DSN").ok(),
131        host: env::var("RELAY_HOST").ok(),
132        port: env::var("RELAY_PORT").ok(),
133        processing: env::var("RELAY_PROCESSING_ENABLED").ok(),
134        kafka_url: env::var("RELAY_KAFKA_BROKER_URL").ok(),
135        redis_url: env::var("RELAY_REDIS_URL").ok(),
136        id: env::var("RELAY_ID").ok(),
137        public_key: env::var("RELAY_PUBLIC_KEY").ok(),
138        secret_key: env::var("RELAY_SECRET_KEY").ok(),
139        outcome_source: None, //already extracted in params
140        shutdown_timeout: env::var("SHUTDOWN_TIMEOUT").ok(),
141        instance: env::var("RELAY_INSTANCE").ok(),
142        server_name: env::var("RELAY_SERVER_NAME")
143            .ok()
144            .or_else(|| env::var("HOSTNAME").ok()),
145    }
146}
147
148pub fn manage_credentials(mut config: Config, matches: &ArgMatches) -> Result<()> {
149    // generate completely new credentials
150    if let Some(matches) = matches.subcommand_matches("generate") {
151        if config.current().has_credentials() && !matches.get_flag("overwrite") {
152            bail!("aborting because credentials already exist. Pass --overwrite to force.");
153        }
154        let credentials = Credentials::generate();
155        if matches.get_flag("stdout") {
156            println!("{}", credentials.to_json_string()?);
157        } else {
158            config.replace_credentials(Some(credentials))?;
159            println!("Generated new credentials");
160            setup::dump_credentials(&config.current());
161        }
162    } else if let Some(matches) = matches.subcommand_matches("set") {
163        let mut prompted = false;
164        let secret_key = match matches.get_one::<String>("secret_key") {
165            Some(value) => Some(
166                value
167                    .parse()
168                    .map_err(|_| anyhow!("invalid secret key supplied"))?,
169            ),
170            None => config.current().credentials().map(|x| x.secret_key.clone()),
171        };
172        let public_key = match matches.get_one::<String>("public_key") {
173            Some(value) => Some(
174                value
175                    .parse()
176                    .map_err(|_| anyhow!("invalid public key supplied"))?,
177            ),
178            None => config.current().credentials().map(|x| x.public_key.clone()),
179        };
180        let id = match matches.get_one::<String>("id").map(String::as_str) {
181            Some("random") => Some(Uuid::new_v4()),
182            Some(value) => Some(
183                value
184                    .parse()
185                    .map_err(|_| anyhow!("invalid relay id supplied"))?,
186            ),
187            None => config.current().credentials().map(|x| x.id),
188        };
189        let changed = config.replace_credentials(Some(Credentials {
190            secret_key: match secret_key {
191                Some(value) => value,
192                None => {
193                    prompted = true;
194                    utils::prompt_value_no_default("secret key")?
195                }
196            },
197            public_key: match public_key {
198                Some(value) => value,
199                None => {
200                    prompted = true;
201                    utils::prompt_value_no_default("public key")?
202                }
203            },
204            id: match id {
205                Some(value) => value,
206                None => {
207                    prompted = true;
208                    if Confirm::with_theme(get_theme())
209                        .with_prompt("do you want to generate a random relay id")
210                        .interact()?
211                    {
212                        Uuid::new_v4()
213                    } else {
214                        utils::prompt_value_no_default("relay id")?
215                    }
216                }
217            },
218        }))?;
219        if !changed {
220            println!("Nothing was changed");
221            if !prompted {
222                println!("Run `relay credentials remove` first to remove all stored credentials.");
223            }
224        } else {
225            println!("Stored updated credentials:");
226            setup::dump_credentials(&config.current());
227        }
228    } else if let Some(matches) = matches.subcommand_matches("remove") {
229        if config.current().has_credentials() {
230            if matches.get_flag("yes")
231                || Confirm::with_theme(get_theme())
232                    .with_prompt("Remove stored credentials?")
233                    .interact()?
234            {
235                config.replace_credentials(None)?;
236                println!("Credentials removed");
237            }
238        } else {
239            println!("No credentials");
240        }
241    } else if matches.subcommand_matches("show").is_some() {
242        if !config.current().has_credentials() {
243            bail!("no stored credentials");
244        } else {
245            println!("Credentials:");
246            setup::dump_credentials(&config.current());
247        }
248    } else {
249        unreachable!();
250    }
251
252    Ok(())
253}
254
255pub fn manage_config(config: &Config, matches: &ArgMatches) -> Result<()> {
256    if let Some(matches) = matches.subcommand_matches("init") {
257        init_config(config.path(), matches)
258    } else if let Some(matches) = matches.subcommand_matches("show") {
259        match matches.get_one("format").map(String::as_str).unwrap() {
260            "debug" => println!("{config:#?}"),
261            "yaml" => println!("{}", config.to_yaml_string()?),
262            _ => unreachable!(),
263        }
264        Ok(())
265    } else {
266        unreachable!();
267    }
268}
269
270pub fn init_config<P: AsRef<Path>>(config_path: P, _matches: &ArgMatches) -> Result<()> {
271    let mut done_something = false;
272    let config_path = env::current_dir()?.join(config_path.as_ref());
273    println!("Initializing relay in {}", config_path.display());
274
275    if !Config::config_exists(&config_path) {
276        let item = Select::with_theme(get_theme())
277            .with_prompt("Do you want to create a new config?")
278            .default(0)
279            .item("Yes, create default config")
280            .item("Yes, create custom config")
281            .item("No, abort")
282            .interact()?;
283
284        let with_prompts = match item {
285            0 => false,
286            1 => true,
287            2 => return Ok(()),
288            _ => unreachable!(),
289        };
290
291        let mut mincfg = MinimalConfig::default();
292        if with_prompts {
293            let mode = Select::with_theme(get_theme())
294                .with_prompt("How should this relay operate?")
295                .default(0)
296                .item("Managed through upstream")
297                .item("Proxy for all events")
298                .interact()?;
299
300            mincfg.relay.mode = match mode {
301                0 => RelayMode::Managed,
302                1 => RelayMode::Proxy,
303                _ => unreachable!(),
304            };
305
306            utils::prompt_value("upstream", &mut mincfg.relay.upstream)?;
307            utils::prompt_value("listen interface", &mut mincfg.relay.host)?;
308            utils::prompt_value("listen port", &mut mincfg.relay.port)?;
309        }
310
311        // TODO: Enable this once logging to Sentry is more useful.
312        // mincfg.sentry.enabled = Select::with_theme(get_theme())
313        //     .with_prompt("Do you want to enable internal crash reporting?")
314        //     .default(0)
315        //     .item("Yes, share relay internal crash reports with sentry.io")
316        //     .item("No, do not share crash reports")
317        //     .interact()?
318        //     == 0;
319
320        mincfg.save_in_folder(&config_path)?;
321        done_something = true;
322    }
323
324    let mut config = Config::from_path(&config_path)?;
325    if config.current().relay_mode() == RelayMode::Managed && !config.current().has_credentials() {
326        let credentials = Credentials::generate();
327        config.replace_credentials(Some(credentials))?;
328        println!("Generated new credentials");
329        setup::dump_credentials(&config.current());
330        done_something = true;
331    }
332
333    if done_something {
334        println!("All done!");
335    } else {
336        println!("Nothing to do.");
337    }
338
339    Ok(())
340}
341
342pub fn generate_completions(matches: &ArgMatches) -> Result<()> {
343    let shell = match matches.get_one::<Shell>("format") {
344        Some(shell) => *shell,
345        None => match env::var("SHELL")
346            .ok()
347            .as_ref()
348            .and_then(|x| x.rsplit('/').next())
349        {
350            Some("bash") => Shell::Bash,
351            Some("zsh") => Shell::Zsh,
352            Some("fish") => Shell::Fish,
353            #[cfg(windows)]
354            _ => Shell::PowerShell,
355            #[cfg(not(windows))]
356            _ => Shell::Bash,
357        },
358    };
359
360    let mut app = make_app();
361    let name = app.get_name().to_owned();
362    clap_complete::generate(shell, &mut app, name, &mut io::stdout());
363
364    Ok(())
365}
366
367pub fn run(config: Config, _matches: &ArgMatches) -> Result<()> {
368    setup::dump_spawn_infos(&config);
369    setup::check_config(&config.current())?;
370    setup::init_metrics(&config.current())?;
371    relay_server::run(config)?;
372    Ok(())
373}