greentic-operator 0.4.43

Greentic operator CLI for local dev and demo orchestration.
Documentation
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,
    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 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(),
                },
            });
        }
    }

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

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("team")), "team");
    }

    #[test]
    fn selects_tenant_team_over_tenant_and_default() {
        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);
    }
}