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
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 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 let arg_config = extract_config_args(matches);
68 config.apply_override(arg_config)?;
69 run(config, matches)
70 } else {
71 unreachable!();
72 }
73}
74
75pub 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
106pub 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, 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 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 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}