greentic-start-dev 1.1.27190108346

Greentic lifecycle runner for start/restart/stop orchestration
Documentation
#![allow(dead_code)]

use std::borrow::Cow;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, anyhow};

use crate::operator_log;
use crate::secrets_backend::{self, SecretsBackendKind};

const OVERRIDE_ENV: &str = "GREENTIC_SECRETS_MANAGER_PACK";
const DEFAULT_SECRETS_DIR: &str = "providers/secrets";

#[derive(Clone, Debug)]
pub struct SecretsManagerSelection {
    pub scope: SelectedKind,
    pub pack_path: Option<PathBuf>,
    pub reason: String,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SelectedKind {
    TenantTeam,
    Tenant,
    Default,
    EnvImplicit,
    Override,
    None,
}

impl SecretsManagerSelection {
    pub fn description(&self) -> String {
        match &self.pack_path {
            Some(path) => format!("{} (pack={})", self.reason, path.display()),
            None => self.reason.clone(),
        }
    }

    pub fn kind(&self) -> Result<SecretsBackendKind> {
        if self.scope == SelectedKind::EnvImplicit {
            return Ok(SecretsBackendKind::Env);
        }
        if let Some(pack_path) = &self.pack_path {
            secrets_backend::backend_kind_from_pack(pack_path)
        } else {
            Ok(SecretsBackendKind::DevStore)
        }
    }
}

pub fn canonical_team<'a>(team: Option<&'a str>) -> Cow<'a, str> {
    match team
        .map(|value| value.trim())
        .filter(|trimmed| !trimmed.is_empty() && !trimmed.eq_ignore_ascii_case("default"))
    {
        Some(value) => Cow::Borrowed(value),
        None => Cow::Borrowed("_"),
    }
}

pub fn select_secrets_manager(
    bundle_root: &Path,
    tenant: &str,
    team: &str,
) -> Result<SecretsManagerSelection> {
    if let Some(override_path) = resolve_override(bundle_root)? {
        return Ok(SecretsManagerSelection {
            scope: SelectedKind::Override,
            pack_path: Some(override_path.clone()),
            reason: format!("override secrets manager pack {}", override_path.display()),
        });
    }

    let candidate_dirs = [
        (
            SelectedKind::TenantTeam,
            bundle_root
                .join(DEFAULT_SECRETS_DIR)
                .join(tenant)
                .join(team),
        ),
        (
            SelectedKind::Tenant,
            bundle_root.join(DEFAULT_SECRETS_DIR).join(tenant),
        ),
        (SelectedKind::Default, bundle_root.join(DEFAULT_SECRETS_DIR)),
    ];

    for (kind, dir) in &candidate_dirs {
        if let Some(pack) = find_best_pack(dir).context("scan secrets manager packs")? {
            return Ok(SecretsManagerSelection {
                scope: *kind,
                pack_path: Some(pack.clone()),
                reason: match kind {
                    SelectedKind::TenantTeam => "tenant/team secrets manager pack".to_string(),
                    SelectedKind::Tenant => "tenant secrets manager pack".to_string(),
                    SelectedKind::Default => "default secrets manager pack".to_string(),
                    _ => "secrets manager pack".to_string(),
                },
            });
        }
    }

    if has_env_secret_vars() {
        return Ok(SecretsManagerSelection {
            scope: SelectedKind::EnvImplicit,
            pack_path: None,
            reason: "environment secrets detected in process environment".to_string(),
        });
    }

    Ok(SecretsManagerSelection {
        scope: SelectedKind::None,
        pack_path: None,
        reason: "no secrets manager pack found".to_string(),
    })
}

fn has_env_secret_vars() -> bool {
    env::vars_os().any(|(key, _)| {
        key.to_str()
            .map(|name| name.starts_with("GREENTIC_SECRET__"))
            .unwrap_or(false)
    })
}

fn resolve_override(bundle_root: &Path) -> Result<Option<PathBuf>> {
    let value = match env::var_os(OVERRIDE_ENV) {
        Some(value) => value,
        None => return Ok(None),
    };
    let candidate = PathBuf::from(value);
    let resolved = if candidate.is_absolute() {
        candidate
    } else {
        bundle_root.join(candidate)
    };
    if !resolved.exists() {
        return Err(anyhow!(
            "override secrets manager pack {} not found",
            resolved.display()
        ));
    }
    Ok(Some(resolved))
}

fn find_best_pack(dir: &Path) -> Result<Option<PathBuf>> {
    if !dir.is_dir() {
        return Ok(None);
    }
    let mut packs = Vec::new();
    for entry in fs::read_dir(dir).with_context(|| format!("read secrets dir {}", dir.display()))? {
        let entry = entry?;
        let path = entry.path();
        if path
            .extension()
            .and_then(|ext| ext.to_str())
            .map(|ext| ext.eq_ignore_ascii_case("gtpack"))
            .unwrap_or(false)
            && path.is_file()
        {
            packs.push(path);
        }
    }
    if packs.is_empty() {
        return Ok(None);
    }
    packs.sort();
    if packs.len() > 1 {
        operator_log::warn(
            module_path!(),
            format!(
                "multiple secrets manager packs found in {}; using {}",
                dir.display(),
                packs[0]
                    .file_name()
                    .and_then(|name| name.to_str())
                    .unwrap_or("unknown")
            ),
        );
    }
    Ok(Some(packs.into_iter().next().unwrap()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::env;
    use tempfile::tempdir;

    #[test]
    fn canonical_team_maps_default_and_empty_to_underscore() {
        assert_eq!(canonical_team(Some("default")), "_");
        assert_eq!(canonical_team(Some("")), "_");
        assert_eq!(canonical_team(Some(" Default ")), "_");
        assert_eq!(canonical_team(Some("team")), "team");
    }

    #[test]
    fn selects_tenant_team_over_tenant_and_default() {
        let _env_guard = crate::test_env_lock().lock().unwrap();
        let dir = tempdir().unwrap();
        let base = dir.path().join(DEFAULT_SECRETS_DIR);
        fs::create_dir_all(base.join("tenant").join("team")).unwrap();
        fs::create_dir_all(base.join("tenant")).unwrap();
        fs::create_dir_all(&base).unwrap();
        let team_pack = base.join("tenant").join("team").join("foo.gtpack");
        fs::write(&team_pack, "").unwrap();
        let tenant_pack = base.join("tenant").join("bar.gtpack");
        fs::write(&tenant_pack, "").unwrap();
        let default_pack = base.join("default.gtpack");
        fs::write(&default_pack, "").unwrap();
        let selection = select_secrets_manager(dir.path(), "tenant", "team").unwrap();
        assert_eq!(selection.scope, SelectedKind::TenantTeam);
        assert_eq!(
            selection.pack_path.unwrap().file_name().unwrap(),
            "foo.gtpack"
        );
    }

    #[test]
    fn override_env_wins() {
        let _env_guard = crate::test_env_lock().lock().unwrap();
        let dir = tempdir().unwrap();
        let alt = dir.path().join("alt.gtpack");
        fs::write(&alt, "").unwrap();
        unsafe {
            env::set_var(OVERRIDE_ENV, alt.strip_prefix(dir.path()).unwrap());
        }
        let selection = select_secrets_manager(dir.path(), "tenant", "team").unwrap();
        unsafe {
            env::remove_var(OVERRIDE_ENV);
        }
        assert_eq!(selection.scope, SelectedKind::Override);
        assert_eq!(selection.pack_path.unwrap(), alt);
    }

    #[test]
    fn auto_selects_env_backend_when_greentic_secret_vars_exist() {
        let _env_guard = crate::test_env_lock().lock().unwrap();
        let dir = tempdir().unwrap();
        unsafe {
            env::set_var(
                "GREENTIC_SECRET__DEMO__ACME_____MESSAGING_WEBCHAT_GUI__JWT_SIGNING_KEY",
                "x",
            );
        }
        let selection = select_secrets_manager(dir.path(), "tenant", "team").unwrap();
        unsafe {
            env::remove_var(
                "GREENTIC_SECRET__DEMO__ACME_____MESSAGING_WEBCHAT_GUI__JWT_SIGNING_KEY",
            );
        }
        assert_eq!(selection.scope, SelectedKind::EnvImplicit);
        assert!(selection.pack_path.is_none());
        assert_eq!(selection.kind().unwrap(), SecretsBackendKind::Env);
    }

    #[test]
    fn override_env_errors_when_pack_is_missing() {
        let _env_guard = crate::test_env_lock().lock().unwrap();
        let dir = tempdir().unwrap();
        unsafe {
            env::set_var(OVERRIDE_ENV, "missing.gtpack");
        }
        let err = select_secrets_manager(dir.path(), "tenant", "team").unwrap_err();
        unsafe {
            env::remove_var(OVERRIDE_ENV);
        }
        assert!(err.to_string().contains("override secrets manager pack"));
    }

    #[test]
    fn selects_lexicographically_first_pack_within_scope() {
        let _env_guard = crate::test_env_lock().lock().unwrap();
        let dir = tempdir().unwrap();
        let base = dir.path().join(DEFAULT_SECRETS_DIR);
        fs::create_dir_all(&base).unwrap();
        let alpha = base.join("alpha.gtpack");
        let zeta = base.join("zeta.gtpack");
        fs::write(&zeta, "").unwrap();
        fs::write(&alpha, "").unwrap();

        let selection = select_secrets_manager(dir.path(), "tenant", "team").unwrap();

        assert_eq!(selection.scope, SelectedKind::Default);
        assert_eq!(selection.pack_path.unwrap(), alpha);
    }
}