1use crate::Error;
27use crate::constants;
28use std::env;
29use std::path::PathBuf;
30
31pub const MODEL_EXPRESS_PREFIX: &str = "MODEL_EXPRESS";
36
37pub const MODEL_EXPRESS_ENDPOINT: &str = "MODEL_EXPRESS_ENDPOINT";
40pub const MODEL_EXPRESS_TIMEOUT: &str = "MODEL_EXPRESS_TIMEOUT";
42pub const MODEL_EXPRESS_CACHE_DIRECTORY: &str = "MODEL_EXPRESS_CACHE_DIRECTORY";
44pub const MODEL_EXPRESS_LOG_LEVEL: &str = "MODEL_EXPRESS_LOG_LEVEL";
46pub const MODEL_EXPRESS_LOG_FORMAT: &str = "MODEL_EXPRESS_LOG_FORMAT";
48pub const MODEL_EXPRESS_MAX_RETRIES: &str = "MODEL_EXPRESS_MAX_RETRIES";
50pub const MODEL_EXPRESS_RETRY_DELAY: &str = "MODEL_EXPRESS_RETRY_DELAY";
52pub const MODEL_EXPRESS_NO_SHARED_STORAGE: &str = "MODEL_EXPRESS_NO_SHARED_STORAGE";
54pub const MODEL_EXPRESS_TRANSFER_CHUNK_SIZE: &str = "MODEL_EXPRESS_TRANSFER_CHUNK_SIZE";
56pub const MODEL_EXPRESS_SERVER_PORT: &str = "MODEL_EXPRESS_SERVER_PORT";
58pub const MODEL_EXPRESS_SERVER_HOST: &str = "MODEL_EXPRESS_SERVER_HOST";
60pub const MODEL_EXPRESS_CACHE_EVICTION_ENABLED: &str = "MODEL_EXPRESS_CACHE_EVICTION_ENABLED";
62pub const MODEL_EXPRESS_SERVER_ENDPOINT: &str = "MODEL_EXPRESS_SERVER_ENDPOINT";
64
65pub const HF_TOKEN: &str = "HF_TOKEN";
68pub const HF_HUB_CACHE: &str = "HF_HUB_CACHE";
70pub const HF_HUB_OFFLINE: &str = "HF_HUB_OFFLINE";
72pub const HF_ENDPOINT: &str = "HF_ENDPOINT";
75
76pub const NGC_API_ENDPOINT: &str = "NGC_API_ENDPOINT";
79pub const NGC_AUTH_ENDPOINT: &str = "NGC_AUTH_ENDPOINT";
81pub const NGC_API_KEY: &str = "NGC_API_KEY";
83pub const NGC_CLI_API_KEY: &str = "NGC_CLI_API_KEY";
85pub const NGC_CLI_HOME: &str = "NGC_CLI_HOME";
87
88pub const DEFAULT_NGC_API_BASE: &str = "https://api.ngc.nvidia.com";
90pub const DEFAULT_NGC_AUTHN_BASE: &str = "https://authn.nvidia.com";
92
93pub const MX_METADATA_BACKEND: &str = "MX_METADATA_BACKEND";
96pub const REDIS_URL: &str = "REDIS_URL";
98pub const MX_REDIS_HOST: &str = "MX_REDIS_HOST";
100pub const REDIS_HOST: &str = "REDIS_HOST";
102pub const MX_REDIS_PORT: &str = "MX_REDIS_PORT";
104pub const REDIS_PORT: &str = "REDIS_PORT";
106pub const MX_METADATA_NAMESPACE: &str = "MX_METADATA_NAMESPACE";
108pub const POD_NAMESPACE: &str = "POD_NAMESPACE";
110pub const POD_NAME: &str = "POD_NAME";
112pub const POD_UID: &str = "POD_UID";
114
115pub const MX_REAPER_SCAN_INTERVAL_SECS: &str = "MX_REAPER_SCAN_INTERVAL_SECS";
118pub const MX_HEARTBEAT_TIMEOUT_SECS: &str = "MX_HEARTBEAT_TIMEOUT_SECS";
120pub const MX_GC_TIMEOUT_SECS: &str = "MX_GC_TIMEOUT_SECS";
122
123pub const HOME: &str = "HOME";
126pub const USERPROFILE: &str = "USERPROFILE";
128pub const KUBECONFIG: &str = "KUBECONFIG";
130
131const DEFAULT_REAPER_SCAN_INTERVAL_SECS: u64 = 30;
133const DEFAULT_HEARTBEAT_TIMEOUT_SECS: u64 = 90;
134const DEFAULT_GC_TIMEOUT_SECS: u64 = 3600;
135
136pub fn home_dir() -> std::result::Result<String, Box<Error>> {
143 env::var(HOME)
144 .or_else(|_| env::var(USERPROFILE))
145 .map_err(|e| Error::Generic(format!("Failed to get home directory: {e}")).into())
146}
147
148pub fn home_dir_or_cwd() -> PathBuf {
150 PathBuf::from(home_dir().unwrap_or_else(|_| ".".to_string()))
151}
152
153pub fn cache_directory() -> Option<PathBuf> {
155 env::var(MODEL_EXPRESS_CACHE_DIRECTORY)
156 .ok()
157 .map(PathBuf::from)
158}
159
160pub fn server_endpoint_or_default() -> String {
163 env::var(MODEL_EXPRESS_SERVER_ENDPOINT)
164 .unwrap_or_else(|_| format!("http://localhost:{}", constants::DEFAULT_GRPC_PORT))
165}
166
167pub fn hf_token() -> Option<String> {
169 env::var(HF_TOKEN).ok()
170}
171
172pub fn hf_hub_cache() -> Option<PathBuf> {
174 env::var(HF_HUB_CACHE).ok().map(PathBuf::from)
175}
176
177pub fn hf_offline() -> bool {
180 env::var(HF_HUB_OFFLINE)
181 .map(|v| matches!(v.to_uppercase().as_str(), "1" | "ON" | "YES" | "TRUE"))
182 .unwrap_or(false)
183}
184
185pub fn ngc_api_base() -> String {
187 env::var(NGC_API_ENDPOINT).unwrap_or_else(|_| DEFAULT_NGC_API_BASE.to_string())
188}
189
190pub fn ngc_authn_base() -> String {
192 env::var(NGC_AUTH_ENDPOINT).unwrap_or_else(|_| DEFAULT_NGC_AUTHN_BASE.to_string())
193}
194
195pub fn ngc_api_key() -> Option<String> {
198 for var in [NGC_API_KEY, NGC_CLI_API_KEY] {
199 if let Ok(v) = env::var(var) {
200 let trimmed = v.trim().to_string();
201 if !trimmed.is_empty() {
202 return Some(trimmed);
203 }
204 }
205 }
206 None
207}
208
209pub fn ngc_cli_home() -> Option<PathBuf> {
211 env::var(NGC_CLI_HOME).ok().map(PathBuf::from)
212}
213
214pub fn metadata_backend() -> String {
216 env::var(MX_METADATA_BACKEND).unwrap_or_default()
217}
218
219pub fn redis_url() -> Option<String> {
221 env::var(REDIS_URL).ok()
222}
223
224pub fn redis_host() -> Option<String> {
226 env::var(MX_REDIS_HOST)
227 .or_else(|_| env::var(REDIS_HOST))
228 .ok()
229}
230
231pub fn redis_port() -> Option<String> {
233 env::var(MX_REDIS_PORT)
234 .or_else(|_| env::var(REDIS_PORT))
235 .ok()
236}
237
238pub fn metadata_namespace() -> Option<String> {
240 env::var(MX_METADATA_NAMESPACE)
241 .or_else(|_| env::var(POD_NAMESPACE))
242 .ok()
243}
244
245pub fn reaper_scan_interval_secs() -> u64 {
247 env_u64(
248 MX_REAPER_SCAN_INTERVAL_SECS,
249 DEFAULT_REAPER_SCAN_INTERVAL_SECS,
250 )
251}
252
253pub fn heartbeat_timeout_secs() -> u64 {
255 env_u64(MX_HEARTBEAT_TIMEOUT_SECS, DEFAULT_HEARTBEAT_TIMEOUT_SECS)
256}
257
258pub fn gc_timeout_secs() -> u64 {
260 env_u64(MX_GC_TIMEOUT_SECS, DEFAULT_GC_TIMEOUT_SECS)
261}
262
263fn env_u64(name: &str, default: u64) -> u64 {
265 env::var(name)
266 .ok()
267 .and_then(|v| v.parse().ok())
268 .unwrap_or(default)
269}
270
271#[cfg(test)]
272#[allow(clippy::expect_used)]
273mod tests {
274 use super::*;
275 use crate::test_support::{EnvVarGuard, acquire_env_mutex};
276
277 #[test]
278 fn name_constants_match_their_literals() {
279 assert_eq!(MODEL_EXPRESS_PREFIX, "MODEL_EXPRESS");
280 assert_eq!(MODEL_EXPRESS_ENDPOINT, "MODEL_EXPRESS_ENDPOINT");
281 assert_eq!(MODEL_EXPRESS_TIMEOUT, "MODEL_EXPRESS_TIMEOUT");
282 assert_eq!(
283 MODEL_EXPRESS_CACHE_DIRECTORY,
284 "MODEL_EXPRESS_CACHE_DIRECTORY"
285 );
286 assert_eq!(MODEL_EXPRESS_LOG_LEVEL, "MODEL_EXPRESS_LOG_LEVEL");
287 assert_eq!(MODEL_EXPRESS_LOG_FORMAT, "MODEL_EXPRESS_LOG_FORMAT");
288 assert_eq!(MODEL_EXPRESS_MAX_RETRIES, "MODEL_EXPRESS_MAX_RETRIES");
289 assert_eq!(MODEL_EXPRESS_RETRY_DELAY, "MODEL_EXPRESS_RETRY_DELAY");
290 assert_eq!(
291 MODEL_EXPRESS_NO_SHARED_STORAGE,
292 "MODEL_EXPRESS_NO_SHARED_STORAGE"
293 );
294 assert_eq!(
295 MODEL_EXPRESS_TRANSFER_CHUNK_SIZE,
296 "MODEL_EXPRESS_TRANSFER_CHUNK_SIZE"
297 );
298 assert_eq!(MODEL_EXPRESS_SERVER_PORT, "MODEL_EXPRESS_SERVER_PORT");
299 assert_eq!(MODEL_EXPRESS_SERVER_HOST, "MODEL_EXPRESS_SERVER_HOST");
300 assert_eq!(
301 MODEL_EXPRESS_CACHE_EVICTION_ENABLED,
302 "MODEL_EXPRESS_CACHE_EVICTION_ENABLED"
303 );
304 assert_eq!(
305 MODEL_EXPRESS_SERVER_ENDPOINT,
306 "MODEL_EXPRESS_SERVER_ENDPOINT"
307 );
308 assert_eq!(HF_TOKEN, "HF_TOKEN");
309 assert_eq!(HF_HUB_CACHE, "HF_HUB_CACHE");
310 assert_eq!(HF_HUB_OFFLINE, "HF_HUB_OFFLINE");
311 assert_eq!(HF_ENDPOINT, "HF_ENDPOINT");
312 assert_eq!(NGC_API_ENDPOINT, "NGC_API_ENDPOINT");
313 assert_eq!(NGC_AUTH_ENDPOINT, "NGC_AUTH_ENDPOINT");
314 assert_eq!(NGC_API_KEY, "NGC_API_KEY");
315 assert_eq!(NGC_CLI_API_KEY, "NGC_CLI_API_KEY");
316 assert_eq!(NGC_CLI_HOME, "NGC_CLI_HOME");
317 assert_eq!(MX_METADATA_BACKEND, "MX_METADATA_BACKEND");
318 assert_eq!(REDIS_URL, "REDIS_URL");
319 assert_eq!(MX_REDIS_HOST, "MX_REDIS_HOST");
320 assert_eq!(REDIS_HOST, "REDIS_HOST");
321 assert_eq!(MX_REDIS_PORT, "MX_REDIS_PORT");
322 assert_eq!(REDIS_PORT, "REDIS_PORT");
323 assert_eq!(MX_METADATA_NAMESPACE, "MX_METADATA_NAMESPACE");
324 assert_eq!(POD_NAMESPACE, "POD_NAMESPACE");
325 assert_eq!(MX_REAPER_SCAN_INTERVAL_SECS, "MX_REAPER_SCAN_INTERVAL_SECS");
326 assert_eq!(MX_HEARTBEAT_TIMEOUT_SECS, "MX_HEARTBEAT_TIMEOUT_SECS");
327 assert_eq!(MX_GC_TIMEOUT_SECS, "MX_GC_TIMEOUT_SECS");
328 assert_eq!(HOME, "HOME");
329 assert_eq!(USERPROFILE, "USERPROFILE");
330 assert_eq!(KUBECONFIG, "KUBECONFIG");
331 assert_eq!(POD_NAME, "POD_NAME");
332 assert_eq!(POD_UID, "POD_UID");
333 }
334
335 #[test]
336 fn hf_offline_parses_truthy_values() {
337 let lock = acquire_env_mutex();
338 for truthy in ["1", "on", "YES", "true", "True"] {
339 let _g = EnvVarGuard::set(&lock, HF_HUB_OFFLINE, truthy);
340 assert!(hf_offline(), "expected {truthy} to enable offline mode");
341 }
342 for falsey in ["0", "off", "no", "maybe"] {
343 let _g = EnvVarGuard::set(&lock, HF_HUB_OFFLINE, falsey);
344 assert!(!hf_offline(), "expected {falsey} to disable offline mode");
345 }
346 let _g = EnvVarGuard::remove(&lock, HF_HUB_OFFLINE);
347 assert!(!hf_offline(), "unset should disable offline mode");
348 }
349
350 #[test]
351 fn ngc_bases_default_then_override() {
352 let lock = acquire_env_mutex();
353 let _api = EnvVarGuard::remove(&lock, NGC_API_ENDPOINT);
354 let _authn = EnvVarGuard::remove(&lock, NGC_AUTH_ENDPOINT);
355 assert_eq!(ngc_api_base(), DEFAULT_NGC_API_BASE);
356 assert_eq!(ngc_authn_base(), DEFAULT_NGC_AUTHN_BASE);
357
358 let _api = EnvVarGuard::set(&lock, NGC_API_ENDPOINT, "https://api.example.com");
359 let _authn = EnvVarGuard::set(&lock, NGC_AUTH_ENDPOINT, "https://authn.example.com");
360 assert_eq!(ngc_api_base(), "https://api.example.com");
361 assert_eq!(ngc_authn_base(), "https://authn.example.com");
362 }
363
364 #[test]
365 fn ngc_api_key_prefers_primary_then_falls_back() {
366 let lock = acquire_env_mutex();
367 let _p = EnvVarGuard::set(&lock, NGC_API_KEY, " primary ");
368 let _s = EnvVarGuard::set(&lock, NGC_CLI_API_KEY, "secondary");
369 assert_eq!(ngc_api_key().as_deref(), Some("primary"));
370
371 let _p = EnvVarGuard::remove(&lock, NGC_API_KEY);
372 assert_eq!(ngc_api_key().as_deref(), Some("secondary"));
373
374 let _s = EnvVarGuard::remove(&lock, NGC_CLI_API_KEY);
375 assert_eq!(ngc_api_key(), None);
376 }
377
378 #[test]
379 fn redis_and_namespace_fallbacks() {
380 let lock = acquire_env_mutex();
381 let _h1 = EnvVarGuard::remove(&lock, MX_REDIS_HOST);
382 let _h2 = EnvVarGuard::set(&lock, REDIS_HOST, "legacy-host");
383 assert_eq!(redis_host().as_deref(), Some("legacy-host"));
384 let _h1 = EnvVarGuard::set(&lock, MX_REDIS_HOST, "mx-host");
385 assert_eq!(redis_host().as_deref(), Some("mx-host"));
386
387 let _n1 = EnvVarGuard::remove(&lock, MX_METADATA_NAMESPACE);
388 let _n2 = EnvVarGuard::set(&lock, POD_NAMESPACE, "pod-ns");
389 assert_eq!(metadata_namespace().as_deref(), Some("pod-ns"));
390 }
391
392 #[test]
393 fn reaper_getters_default_parse_and_fallback() {
394 let lock = acquire_env_mutex();
395 let _g = EnvVarGuard::remove(&lock, MX_REAPER_SCAN_INTERVAL_SECS);
396 assert_eq!(
397 reaper_scan_interval_secs(),
398 DEFAULT_REAPER_SCAN_INTERVAL_SECS
399 );
400
401 let _g = EnvVarGuard::set(&lock, MX_HEARTBEAT_TIMEOUT_SECS, "120");
402 assert_eq!(heartbeat_timeout_secs(), 120);
403
404 let _g = EnvVarGuard::set(&lock, MX_GC_TIMEOUT_SECS, "not-a-number");
405 assert_eq!(gc_timeout_secs(), DEFAULT_GC_TIMEOUT_SECS);
406 }
407}