auxon_sdk/reflector_config/
resolve.rs

1use crate::auth_token::{
2    decode_auth_token_hex, token_user_file::REFLECTOR_AUTH_TOKEN_DEFAULT_FILE_NAME, AuthToken,
3};
4use crate::reflector_config::{try_from_file, Config, ConfigLoadError, CONFIG_ENV_VAR};
5use std::{
6    env,
7    path::{Path, PathBuf},
8};
9
10const CONFIG_FILE_NAME: &str = "config.toml";
11const CONFIG_DIR: &str = "modality-reflector";
12const SYS_CONFIG_BASE_PATH: &str = "/etc";
13
14pub const MODALITY_HOST_ENV_VAR: &str = "MODALITY_HOST";
15pub const MODALITY_REFLECTOR_PLUGINS_DIR_ENV_VAR: &str = "MODALITY_REFLECTOR_PLUGINS_DIR";
16
17/// Load a Config and auth token. Either path may be given explicitly; if not, they are loaded from the
18/// default system and user profile directories. (see `load_config` and `resolve_reflector_auth_token`).
19/// Environment variable overrides are automatically incorporated. (See `ConfigContext::apply_environment_variable_overrides`)
20pub fn load_config_and_auth_token(
21    manually_provided_config_path: Option<PathBuf>,
22    manually_provided_auth_token_path: Option<PathBuf>,
23) -> Result<
24    (crate::reflector_config::refined::Config, AuthToken),
25    Box<dyn std::error::Error + Send + Sync>,
26> {
27    let ConfigContext {
28        config: cfg,
29        config_file_parent_dir,
30        ..
31    } = ConfigContext::load_default(manually_provided_config_path)?;
32
33    let auth_token =
34        resolve_reflector_auth_token(manually_provided_auth_token_path, &config_file_parent_dir)?;
35    Ok((cfg, auth_token))
36}
37
38/// Attempt to load a `config.toml` configuration file from the following locations:
39/// - system configuration directory (i.e. /etc/modality-reflector/config.toml on Linux)
40/// - `dirs::config_dir()` (i.e. ~/.config/modality-reflector/config.toml on Linux)
41/// - Environment variable `MODALITY_REFLECTOR_CONFIG`
42/// - Manually provided path (i.e. at the CLI with `--config file`)
43///
44/// The files are read in the order given above, with last file found
45/// taking precedence over files read earlier.
46///
47/// If a configuration file doesn't exists in any of the locations, None is returned.
48pub fn load_config(
49    manually_provided_config_path: Option<PathBuf>,
50) -> Result<Option<ConfigContext>, ExpandedConfigLoadError> {
51    let mut cfg = load_system_config()?;
52    if let Some(other_cfg) = load_user_config()? {
53        cfg.replace(other_cfg);
54    }
55    if let Some(other_cfg) = load_env_config()? {
56        cfg.replace(other_cfg);
57    }
58    if let Some(other_cfg_path) = manually_provided_config_path {
59        if let Some(config_file_parent_dir) = other_cfg_path.parent().map(ToOwned::to_owned) {
60            let other_cfg = ConfigContext {
61                config: try_from_file(other_cfg_path.as_path())?,
62                config_file: Some(other_cfg_path),
63                config_file_parent_dir,
64            };
65            cfg.replace(other_cfg);
66        }
67    }
68    Ok(cfg)
69}
70
71pub struct ConfigContext {
72    pub config: Config,
73    pub config_file: Option<PathBuf>,
74    pub config_file_parent_dir: PathBuf,
75}
76
77impl ConfigContext {
78    pub fn load_default(
79        config_file_override: Option<PathBuf>,
80    ) -> Result<Self, ExpandedConfigLoadError> {
81        let mut cc = if let Some(cc) = load_config(config_file_override)? {
82            cc
83        } else {
84            let config_file_parent_dir = env::current_dir().map_err(|ioerr| {
85                ExpandedConfigLoadError::ConfigLoadError(ConfigLoadError::Io(ioerr))
86            })?;
87            ConfigContext {
88                config: Default::default(),
89                config_file: None,
90                config_file_parent_dir,
91            }
92        };
93
94        cc.apply_environment_variable_overrides()?;
95
96        Ok(cc)
97    }
98
99    /// Override values in this configuration with values from environment variables, if they are set.
100    ///
101    /// * `MODALITY_HOST`: Overrides `ingest.protocol_parent_url` and
102    ///   `mutation.protocol_parent_url`, to connect to this host, on
103    ///   the default port.
104    /// * `MODALITY_REFLECTOR_PLUGINS_DIR`: Overrides `ingest.plugins.plugins_dir` to load plugins
105    ///   from the provided directory.
106    pub fn apply_environment_variable_overrides(&mut self) -> Result<(), ExpandedConfigLoadError> {
107        if let Some(modality_host) = env_str(MODALITY_HOST_ENV_VAR)? {
108            if self.config.ingest.is_none() {
109                self.config.ingest = Some(Default::default());
110            }
111
112            let ingest = self.config.ingest.as_mut().unwrap();
113            ingest.protocol_parent_url = Some(
114                url::Url::parse(&format!("modality-ingest://{modality_host}")).map_err(|_| {
115                    ExpandedConfigLoadError::InvalidHostNameFromEnv {
116                        var: MODALITY_HOST_ENV_VAR,
117                        value: modality_host.clone(),
118                    }
119                })?,
120            );
121
122            if self.config.mutation.is_none() {
123                self.config.mutation = Some(Default::default());
124            }
125
126            let mutation = self.config.mutation.as_mut().unwrap();
127            mutation.protocol_parent_url = Some(
128                url::Url::parse(&format!("modality-mutation://{modality_host}")).map_err(|_| {
129                    ExpandedConfigLoadError::InvalidHostNameFromEnv {
130                        var: MODALITY_HOST_ENV_VAR,
131                        value: modality_host,
132                    }
133                })?,
134            );
135        }
136
137        if let Some(plugins_dir) = env_str(MODALITY_REFLECTOR_PLUGINS_DIR_ENV_VAR)? {
138            if self.config.plugins.is_none() {
139                self.config.plugins = Some(Default::default());
140            }
141            let plugins = self.config.plugins.as_mut().unwrap();
142            plugins.plugins_dir = Some(plugins_dir.into());
143        }
144
145        Ok(())
146    }
147}
148
149fn load_system_config() -> Result<Option<ConfigContext>, ConfigLoadError> {
150    let cfg_path = system_config_path();
151    if cfg_path.exists() {
152        tracing::trace!("Load system configuration file {}", cfg_path.display());
153        if let Some(config_file_parent_dir) = cfg_path.parent().map(ToOwned::to_owned) {
154            Ok(Some(ConfigContext {
155                config: try_from_file(&cfg_path)?,
156                config_file: Some(cfg_path),
157                config_file_parent_dir,
158            }))
159        } else {
160            Ok(None)
161        }
162    } else {
163        Ok(None)
164    }
165}
166
167fn load_user_config() -> Result<Option<ConfigContext>, ExpandedConfigLoadError> {
168    load_user_or_env_config(UserOrEnvPath::User)
169}
170
171fn load_env_config() -> Result<Option<ConfigContext>, ExpandedConfigLoadError> {
172    load_user_or_env_config(UserOrEnvPath::Env)
173}
174
175fn load_user_or_env_config(
176    loc: UserOrEnvPath,
177) -> Result<Option<ConfigContext>, ExpandedConfigLoadError> {
178    let cfg_path = match loc {
179        UserOrEnvPath::User => user_config_path(),
180        UserOrEnvPath::Env => env_config_path(),
181    };
182    match cfg_path {
183        Some(p) if p.exists() => {
184            tracing::trace!("Load {} configuration file {}", loc, p.display());
185            if let Some(config_file_parent_dir) = p.as_path().parent().map(ToOwned::to_owned) {
186                Ok(Some(ConfigContext {
187                    config: try_from_file(&p)?,
188                    config_file: Some(p),
189                    config_file_parent_dir,
190                }))
191            } else {
192                Ok(None)
193            }
194        }
195        _ => Ok(None),
196    }
197}
198
199fn system_config_path() -> PathBuf {
200    PathBuf::from(SYS_CONFIG_BASE_PATH)
201        .join(CONFIG_DIR)
202        .join(CONFIG_FILE_NAME)
203}
204
205fn user_config_path() -> Option<PathBuf> {
206    dirs::config_dir().map(|d| d.join(CONFIG_DIR).join(CONFIG_FILE_NAME))
207}
208
209fn env_config_path() -> Option<PathBuf> {
210    env::var_os(CONFIG_ENV_VAR).map(PathBuf::from)
211}
212
213fn env_str(var: &'static str) -> Result<Option<String>, ExpandedConfigLoadError> {
214    match env::var(var) {
215        Ok(s) => Ok(Some(s)),
216        Err(env::VarError::NotPresent) => Ok(None),
217        Err(env::VarError::NotUnicode(_)) => Err(ExpandedConfigLoadError::NonUnicodeEnvVar { var }),
218    }
219}
220
221#[derive(Debug, thiserror::Error)]
222pub enum ExpandedConfigLoadError {
223    #[error("Configuration environment variable '{var}' contained a non-unicode value")]
224    NonUnicodeEnvVar { var: &'static str },
225
226    #[error("Invalid hostname '{value}' specified in environment variable '{var}'")]
227    InvalidHostNameFromEnv { var: &'static str, value: String },
228
229    #[error("Config loading error.")]
230    ConfigLoadError(
231        #[source]
232        #[from]
233        ConfigLoadError,
234    ),
235}
236
237#[derive(Copy, Clone, Debug)]
238enum UserOrEnvPath {
239    User,
240    Env,
241}
242
243impl std::fmt::Display for UserOrEnvPath {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        match self {
246            UserOrEnvPath::User => f.write_str("user"),
247            UserOrEnvPath::Env => f.write_str("environment"),
248        }
249    }
250}
251
252/// * CLI path override
253/// * Env-Var MODALITY_AUTH_TOKEN
254/// * file relative to process current working directory
255/// * file relative to config file parent directory
256pub fn resolve_reflector_auth_token(
257    cli_override_auth_token_file_path: Option<PathBuf>,
258    config_file_parent_directory: &Path,
259) -> Result<AuthToken, Box<dyn std::error::Error + Send + Sync>> {
260    if let Some(path) = cli_override_auth_token_file_path {
261        if !path.exists() {
262            return Err(ReflectorAuthTokenLoadError::CliProvidedAuthTokenFileDidNotExist.into());
263        }
264        if let Some(token_file_contents) =
265            crate::auth_token::token_user_file::read_user_auth_token_file(&path)?
266        {
267            return Ok(token_file_contents.auth_token);
268        }
269    }
270
271    match env::var(crate::auth_token::MODALITY_AUTH_TOKEN_ENV_VAR) {
272        Ok(val) => {
273            tracing::trace!("Loading CLI env context auth token");
274            return Ok(decode_auth_token_hex(&val)?);
275        }
276        Err(env::VarError::NotUnicode(_)) => {
277            return Err(
278                ReflectorAuthTokenLoadError::EnvVarSpecifiedModalityAuthTokenNonUtf8.into(),
279            );
280        }
281        Err(env::VarError::NotPresent) => {
282            // Fall through and try the next approach
283        }
284    }
285    if let Ok(cwd) = std::env::current_dir() {
286        let cwd_relative_path = cwd.join(REFLECTOR_AUTH_TOKEN_DEFAULT_FILE_NAME);
287        if cwd_relative_path.exists() {
288            if let Some(token_file_contents) =
289                crate::auth_token::token_user_file::read_user_auth_token_file(&cwd_relative_path)?
290            {
291                return Ok(token_file_contents.auth_token);
292            }
293        }
294    }
295
296    let config_relative_path =
297        config_file_parent_directory.join(REFLECTOR_AUTH_TOKEN_DEFAULT_FILE_NAME);
298
299    if let Some(token_file_contents) =
300        crate::auth_token::token_user_file::read_user_auth_token_file(&config_relative_path)?
301    {
302        return Ok(token_file_contents.auth_token);
303    }
304
305    // read the modality cli auth token as a fallback
306    if let Ok(auth_token) = AuthToken::load() {
307        return Ok(auth_token);
308    }
309
310    Err(ReflectorAuthTokenLoadError::Underspecified.into())
311}
312
313#[derive(Debug, thiserror::Error)]
314pub enum ReflectorAuthTokenLoadError {
315    #[error("CLI provided auth token file did not exist")]
316    CliProvidedAuthTokenFileDidNotExist,
317
318    #[error(
319        "The MODALITY_AUTH_TOKEN environment variable contained a non-UTF-8-compatible string"
320    )]
321    EnvVarSpecifiedModalityAuthTokenNonUtf8,
322
323    #[error("No auth token was specified.  Provide a path to a token file as a CLI argument or put the token hex contents into the MODALITY_AUTH_TOKEN environment path")]
324    Underspecified,
325}