use anyhow::{Context, Result, anyhow};
use dotenv::from_path;
use std::path::{Path, PathBuf};
use tracing::{info, warn};
use crate::bootstrap::{Bootstrap, RuntimeConfigMetadata, build_shared_state};
use crate::config::{Config, ConfigLocation, DEFAULT_CONFIG_FILE_NAME};
use crate::config_validation::initialize_runtime_env_settings;
const DEFAULT_PIPELINES_PATH: &str = "config/pipelines.yaml";
pub struct RuntimeBootstrapContext {
pub bootstrap: Bootstrap,
pub config: Config,
pub pipelines_path: String,
pub resolved_config_path: PathBuf,
pub runtime_config_metadata: RuntimeConfigMetadata,
}
pub async fn load_runtime_bootstrap_from_env() -> Result<RuntimeBootstrapContext> {
let config_override = std::env::var("ATHENA_CONFIG_PATH")
.ok()
.filter(|value| !value.trim().is_empty())
.map(PathBuf::from);
let pipelines_path = DEFAULT_PIPELINES_PATH.to_string();
let (config, resolved_config_path, source_label, seeded_default) =
load_runtime_config(config_override.as_ref())?;
load_dotenv_beside_config_file(&resolved_config_path);
initialize_runtime_env_settings(&config.validation_ranges);
let runtime_config_metadata = RuntimeConfigMetadata {
config_path: Some(resolved_config_path.to_string_lossy().to_string()),
source_label: Some(source_label),
seeded_default,
};
let bootstrap = build_shared_state(&config, &pipelines_path, runtime_config_metadata.clone())
.await
.context("failed to build shared Athena runtime state")?;
info!(
config_path = %resolved_config_path.display(),
pipelines_path = %pipelines_path,
"Loaded shared Athena runtime bootstrap"
);
Ok(RuntimeBootstrapContext {
bootstrap,
config,
pipelines_path,
resolved_config_path,
runtime_config_metadata,
})
}
fn load_runtime_config(
config_override: Option<&PathBuf>,
) -> Result<(Config, PathBuf, String, bool)> {
if let Some(path) = config_override {
let config = Config::load_from(path).map_err(|err| {
let attempted = vec![ConfigLocation::new(
"explicit ATHENA_CONFIG_PATH".to_string(),
path.clone(),
)];
anyhow!(
"failed to load config '{}': {}. Looked in:\n{}",
path.display(),
err,
format_attempted_locations(&attempted)
)
})?;
return Ok((
config,
path.clone(),
"ATHENA_CONFIG_PATH".to_string(),
false,
));
}
let outcome = Config::load_default().map_err(|err| {
anyhow!(
"failed to load config '{}': {}. Looked in:\n{}",
DEFAULT_CONFIG_FILE_NAME,
err,
format_attempted_locations(&err.attempted_locations)
)
})?;
let source_label = outcome
.attempted_locations
.iter()
.find(|location| location.path == outcome.path)
.map(|location| location.label.clone())
.unwrap_or_else(|| "auto-discovered config path".to_string());
Ok((
outcome.config,
outcome.path,
source_label,
outcome.seeded_default,
))
}
fn config_sidecar_dotenv_path(config_file: &Path) -> PathBuf {
config_file
.parent()
.unwrap_or_else(|| Path::new("."))
.join(".env")
}
fn load_dotenv_beside_config_file(config_file: &Path) {
let env_path = config_sidecar_dotenv_path(config_file);
if !env_path.exists() {
return;
}
match from_path(&env_path) {
Ok(()) => info!(path = %env_path.display(), "Loaded supplementary .env beside config file"),
Err(err) => warn!(
path = %env_path.display(),
error = %err,
"Found .env beside config file but failed to parse it; ignoring"
),
}
}
fn format_attempted_locations(locations: &[ConfigLocation]) -> String {
locations
.iter()
.map(|location| format!("- {}", location.describe()))
.collect::<Vec<_>>()
.join("\n")
}