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
57 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 let arg_config = extract_config_args(matches);
72 config.apply_override(arg_config)?;
73 run(config, matches)
74 } else {
75 unreachable!();
76 }
77}
78
79pub 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
110pub 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, 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 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 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}