bucketwarden-cli 0.1.0

BucketWarden CLI command parsing, demos, and listener runtime.
Documentation
use crate::operator_cli::types::OperatorConfigFile;
use anyhow::Context;
use bucketwarden_server::RuntimeConfig;

#[derive(Clone, Debug)]
pub struct RuntimeConfigSelection {
    pub config: RuntimeConfig,
    pub source: &'static str,
    pub source_path: Option<String>,
}

const BUCKETWARDEN_CONFIG_ENV: &str = "BUCKETWARDEN_CONFIG";
const CONFIG_SOURCE_DEFAULT: &str = "default";
const CONFIG_SOURCE_ENV: &str = "environment";
const CONFIG_SOURCE_EXPLICIT: &str = "explicit";

pub fn load_runtime_config(path: Option<&str>) -> anyhow::Result<RuntimeConfig> {
    Ok(resolve_runtime_config(path)?.config)
}

pub fn load_runtime_config_with_source(
    path: Option<&str>,
) -> anyhow::Result<RuntimeConfigSelection> {
    resolve_runtime_config(path)
}

pub(crate) fn resolve_runtime_config(path: Option<&str>) -> anyhow::Result<RuntimeConfigSelection> {
    let Some(path) = path else {
        if let Ok(env_path) = std::env::var(BUCKETWARDEN_CONFIG_ENV) {
            let env_path = env_path.trim().to_string();
            if !env_path.is_empty() {
                return parse_runtime_config_from_path(&env_path, CONFIG_SOURCE_ENV);
            }
        }
        return Ok(RuntimeConfigSelection {
            config: RuntimeConfig::development(),
            source: CONFIG_SOURCE_DEFAULT,
            source_path: None,
        });
    };

    parse_runtime_config_from_path(path, CONFIG_SOURCE_EXPLICIT)
}

pub(crate) fn parse_runtime_config_from_path(
    path: &str,
    source: &'static str,
) -> anyhow::Result<RuntimeConfigSelection> {
    let payload = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read runtime config from `{path}`"))?;
    let config: OperatorConfigFile = serde_json::from_str(&payload)
        .with_context(|| format!("invalid runtime config JSON at `{path}`"))?;
    validate_config_fields(&config)?;
    Ok(RuntimeConfigSelection {
        config: RuntimeConfig {
            key_id: config.key_id,
            key_material: config.key_material.into_bytes(),
            clock_epoch_seconds: config.clock_epoch_seconds,
        },
        source,
        source_path: Some(path.to_string()),
    })
}

pub(crate) fn validate_config_fields(config: &OperatorConfigFile) -> anyhow::Result<()> {
    if config.key_id.trim().is_empty() {
        anyhow::bail!("config key_id must not be empty");
    }
    if config.key_material.is_empty() {
        anyhow::bail!("config key_material must not be empty");
    }
    Ok(())
}