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