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