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