modality_reflector_config/
resolve.rs1use std::{
2 env,
3 path::{Path, PathBuf},
4};
5
6use modality_auth_token::{
7 decode_auth_token_hex, token_user_file::REFLECTOR_AUTH_TOKEN_DEFAULT_FILE_NAME, AuthToken,
8};
9
10use crate::{try_from_file, Config, ConfigLoadError, CONFIG_ENV_VAR};
11
12const CONFIG_FILE_NAME: &str = "config.toml";
13const CONFIG_DIR: &str = "modality-reflector";
14const SYS_CONFIG_BASE_PATH: &str = "/etc";
15
16pub fn load_config_and_auth_token(
17 manually_provided_config_path: Option<PathBuf>,
18 manually_provided_auth_token: Option<PathBuf>,
19) -> Result<(crate::refined::Config, AuthToken), Box<dyn std::error::Error + Send + Sync>> {
20 let ConfigContext {
21 config: cfg,
22 config_file_parent_dir,
23 ..
24 } = ConfigContext::load_default(manually_provided_config_path)?;
25
26 let auth_token =
27 resolve_reflector_auth_token(manually_provided_auth_token, &config_file_parent_dir)?;
28 Ok((cfg, auth_token))
29}
30
31pub fn load_config(
42 manually_provided_config_path: Option<PathBuf>,
43) -> Result<Option<ConfigContext>, ExpandedConfigLoadError> {
44 let mut cfg = load_system_config()?;
45 if let Some(other_cfg) = load_user_config()? {
46 cfg.replace(other_cfg);
47 }
48 if let Some(other_cfg) = load_env_config()? {
49 cfg.replace(other_cfg);
50 }
51 if let Some(other_cfg_path) = manually_provided_config_path {
52 if let Some(config_file_parent_dir) = other_cfg_path.parent().map(ToOwned::to_owned) {
53 let other_cfg = ConfigContext {
54 config: try_from_file(other_cfg_path.as_path())?,
55 config_file: Some(other_cfg_path),
56 config_file_parent_dir,
57 };
58 cfg.replace(other_cfg);
59 }
60 }
61 Ok(cfg)
62}
63
64pub struct ConfigContext {
65 pub config: Config,
66 pub config_file: Option<PathBuf>,
67 pub config_file_parent_dir: PathBuf,
68}
69
70impl ConfigContext {
71 pub fn load_default(
72 config_file_override: Option<PathBuf>,
73 ) -> Result<Self, ExpandedConfigLoadError> {
74 if let Some(cc) = load_config(config_file_override)? {
75 Ok(cc)
76 } else {
77 let config_file_parent_dir = env::current_dir().map_err(|ioerr| {
78 ExpandedConfigLoadError::ConfigLoadError(ConfigLoadError::Io(ioerr))
79 })?;
80 Ok(ConfigContext {
81 config: Default::default(),
82 config_file: None,
83 config_file_parent_dir,
84 })
85 }
86 }
87}
88
89fn load_system_config() -> Result<Option<ConfigContext>, ConfigLoadError> {
90 let cfg_path = system_config_path();
91 if cfg_path.exists() {
92 tracing::trace!("Load system configuration file {}", cfg_path.display());
93 if let Some(config_file_parent_dir) = cfg_path.parent().map(ToOwned::to_owned) {
94 Ok(Some(ConfigContext {
95 config: try_from_file(&cfg_path)?,
96 config_file: Some(cfg_path),
97 config_file_parent_dir,
98 }))
99 } else {
100 Ok(None)
101 }
102 } else {
103 Ok(None)
104 }
105}
106
107fn load_user_config() -> Result<Option<ConfigContext>, ExpandedConfigLoadError> {
108 load_user_or_env_config(UserOrEnvPath::User)
109}
110
111fn load_env_config() -> Result<Option<ConfigContext>, ExpandedConfigLoadError> {
112 load_user_or_env_config(UserOrEnvPath::Env)
113}
114
115fn load_user_or_env_config(
116 loc: UserOrEnvPath,
117) -> Result<Option<ConfigContext>, ExpandedConfigLoadError> {
118 let cfg_path = match loc {
119 UserOrEnvPath::User => user_config_path(),
120 UserOrEnvPath::Env => env_config_path()?,
121 };
122 match cfg_path {
123 Some(p) if p.exists() => {
124 tracing::trace!("Load {} configuration file {}", loc, p.display());
125 if let Some(config_file_parent_dir) = p.as_path().parent().map(ToOwned::to_owned) {
126 Ok(Some(ConfigContext {
127 config: try_from_file(&p)?,
128 config_file: Some(p),
129 config_file_parent_dir,
130 }))
131 } else {
132 Ok(None)
133 }
134 }
135 _ => Ok(None),
136 }
137}
138
139fn system_config_path() -> PathBuf {
140 PathBuf::from(SYS_CONFIG_BASE_PATH)
141 .join(CONFIG_DIR)
142 .join(CONFIG_FILE_NAME)
143}
144
145fn user_config_path() -> Option<PathBuf> {
146 dirs::config_dir().map(|d| d.join(CONFIG_DIR).join(CONFIG_FILE_NAME))
147}
148
149fn env_config_path() -> Result<Option<PathBuf>, ExpandedConfigLoadError> {
150 match env::var(CONFIG_ENV_VAR) {
151 Ok(env_path) => Ok(PathBuf::from(env_path).into()),
152 Err(env::VarError::NotPresent) => Ok(None),
153 Err(env::VarError::NotUnicode(_)) => {
154 Err(ExpandedConfigLoadError::EnvVarSpecifiedConfigNonUtf8)
155 }
156 }
157}
158
159#[derive(Debug, thiserror::Error)]
160pub enum ExpandedConfigLoadError {
161 #[error(
162 "The {} environment variable contained a non-UTF-8-compatible path.",
163 CONFIG_ENV_VAR
164 )]
165 EnvVarSpecifiedConfigNonUtf8,
166 #[error("Config loading error.")]
167 ConfigLoadError(
168 #[source]
169 #[from]
170 ConfigLoadError,
171 ),
172}
173
174#[derive(Copy, Clone, Debug)]
175enum UserOrEnvPath {
176 User,
177 Env,
178}
179
180impl std::fmt::Display for UserOrEnvPath {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 match self {
183 UserOrEnvPath::User => f.write_str("user"),
184 UserOrEnvPath::Env => f.write_str("environment"),
185 }
186 }
187}
188
189pub fn resolve_reflector_auth_token(
194 cli_override_auth_token_file_path: Option<PathBuf>,
195 config_file_parent_directory: &Path,
196) -> Result<AuthToken, Box<dyn std::error::Error + Send + Sync>> {
197 if let Some(path) = cli_override_auth_token_file_path {
198 if !path.exists() {
199 return Err(ReflectorAuthTokenLoadError::CliProvidedAuthTokenFileDidNotExist.into());
200 }
201 if let Some(token_file_contents) =
202 modality_auth_token::token_user_file::read_user_auth_token_file(&path)?
203 {
204 return Ok(token_file_contents.auth_token);
205 }
206 }
207
208 match env::var("MODALITY_AUTH_TOKEN") {
209 Ok(val) => {
210 tracing::trace!("Loading CLI env context auth token");
211 return Ok(decode_auth_token_hex(&val)?);
212 }
213 Err(env::VarError::NotUnicode(_)) => {
214 return Err(
215 ReflectorAuthTokenLoadError::EnvVarSpecifiedModalityAuthTokenNonUtf8.into(),
216 );
217 }
218 Err(env::VarError::NotPresent) => {
219 }
221 }
222 if let Ok(cwd) = std::env::current_dir() {
223 let cwd_relative_path = cwd.join(REFLECTOR_AUTH_TOKEN_DEFAULT_FILE_NAME);
224 if cwd_relative_path.exists() {
225 if let Some(token_file_contents) =
226 modality_auth_token::token_user_file::read_user_auth_token_file(&cwd_relative_path)?
227 {
228 return Ok(token_file_contents.auth_token);
229 }
230 }
231 }
232
233 let config_relative_path =
234 config_file_parent_directory.join(REFLECTOR_AUTH_TOKEN_DEFAULT_FILE_NAME);
235
236 if let Some(token_file_contents) =
237 modality_auth_token::token_user_file::read_user_auth_token_file(&config_relative_path)?
238 {
239 return Ok(token_file_contents.auth_token);
240 }
241
242 if let Ok(auth_token) = AuthToken::load() {
244 return Ok(auth_token);
245 }
246
247 Err(ReflectorAuthTokenLoadError::Underspecified.into())
248}
249
250#[derive(Debug, thiserror::Error)]
251pub enum ReflectorAuthTokenLoadError {
252 #[error("CLI provided auth token file did not exist")]
253 CliProvidedAuthTokenFileDidNotExist,
254
255 #[error(
256 "The MODALITY_AUTH_TOKEN environment variable contained a non-UTF-8-compatible string"
257 )]
258 EnvVarSpecifiedModalityAuthTokenNonUtf8,
259
260 #[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")]
261 Underspecified,
262}