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
34pub 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 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 let mut config = load_config(config_path, matches.contains_id("config"))?;
53 let env_config = extract_config_env_vars();
55 config.apply_override(env_config)?;
56 let global_config = extract_global_config_args(&matches);
58 config.apply_override(global_config)?;
59
60 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 let arg_config = extract_config_args(matches);
76 config.apply_override(arg_config)?;
77 run(config, matches)
78 } else {
79 unreachable!();
80 }
81}
82
83pub 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
92pub 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
123pub 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, 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 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 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}