use crate::platform::secrets::SecretsManager;
use crate::store::Db;
use anyhow::Result;
use shared::{EnvVarValue, Job, Service};
use std::collections::HashMap;
use tracing::{debug, error, warn};
async fn secret_value(
secrets: &SecretsManager,
project_id: &str,
service_id: Option<&str>,
secret_name: &str,
env_key: &str,
owner: &str,
) -> Result<String> {
secrets
.resolve(project_id, service_id, secret_name)
.await
.map_err(|e| {
let onde = match service_id {
Some(sid) => format!("nos secrets do serviço '{sid}' e do projeto '{project_id}'"),
None => format!("nos secrets do projeto '{project_id}'"),
};
anyhow::anyhow!(
"a env '{env_key}' ({owner}) aponta para o secret '{secret_name}', que não pôde ser resolvido \
(procurado {onde}): {e}"
)
})
}
async fn resolve_project_env(
db: &Db,
secrets: &SecretsManager,
project_id: &str,
) -> Result<HashMap<String, String>> {
let project_env = match crate::store::projects::get(db, project_id).await {
Ok(Some(project)) => project.env_vars,
Ok(None) => {
warn!(
project_id,
"resolve_env: projeto não encontrado no banco — env vars de projeto não serão injetadas"
);
vec![]
}
Err(e) => {
error!(
project_id,
error = %e,
"resolve_env: falha ao carregar projeto (possível erro de desserialização do JSON env_vars) — env vars de projeto não serão injetadas"
);
vec![]
}
};
let mut env_map: HashMap<String, String> = HashMap::new();
for ev in &project_env {
let value = match &ev.value {
EnvVarValue::Plain(v) => v.clone(),
EnvVarValue::Secret(name) => {
debug!(project_id, secret = %name, "resolve_env: desencriptando secret do projeto");
secret_value(secrets, project_id, None, name, &ev.key, "projeto").await?
}
};
env_map.insert(ev.key.clone(), value);
}
Ok(env_map)
}
pub async fn resolve(
db: &Db,
secrets: &SecretsManager,
svc: &Service,
) -> Result<Vec<(String, String)>> {
let mut env_map = resolve_project_env(db, secrets, &svc.spec.project_id).await?;
for ev in &svc.spec.env_vars {
let value = match &ev.value {
EnvVarValue::Plain(v) => v.clone(),
EnvVarValue::Secret(name) => {
debug!(service_id = %svc.id, secret = %name, "resolve_env: desencriptando secret do serviço");
secret_value(
secrets,
&svc.spec.project_id,
Some(&svc.id),
name,
&ev.key,
"serviço",
)
.await?
}
};
env_map.insert(ev.key.clone(), value);
}
let keys: Vec<&str> = env_map.keys().map(|k| k.as_str()).collect();
tracing::info!(
service_id = %svc.id,
service_vars = svc.spec.env_vars.len(),
total = env_map.len(),
keys = ?keys,
"resolve_env: vars resolvidas (projeto + serviço)"
);
Ok(env_map.into_iter().collect())
}
pub async fn resolve_project_only(
db: &Db,
secrets: &SecretsManager,
project_id: &str,
) -> Result<Vec<(String, String)>> {
let env_map = resolve_project_env(db, secrets, project_id).await?;
tracing::info!(
project_id,
total = env_map.len(),
"resolve_env: vars resolvidas (só projeto, job autônomo sem serviço gatilho)"
);
Ok(env_map.into_iter().collect())
}
pub async fn resolve_job(
db: &Db,
secrets: &SecretsManager,
job: &Job,
) -> Result<Vec<(String, String)>> {
let base = match &job.trigger_service_id {
Some(sid) => {
let svc = crate::store::services::get(db, sid)
.await?
.ok_or_else(|| anyhow::anyhow!("serviço gatilho não encontrado: {sid}"))?;
resolve(db, secrets, &svc).await?
}
None => resolve_project_only(db, secrets, &job.project_id).await?,
};
let mut env_map: HashMap<String, String> = base.into_iter().collect();
for ev in &job.env_vars {
let value = match &ev.value {
EnvVarValue::Plain(v) => v.clone(),
EnvVarValue::Secret(name) => {
debug!(job_id = %job.id, secret = %name, "resolve_env: desencriptando secret do job");
secret_value(
secrets,
&job.project_id,
job.trigger_service_id.as_deref(),
name,
&ev.key,
"job",
)
.await?
}
};
env_map.insert(ev.key.clone(), value);
}
tracing::info!(
job_id = %job.id,
job_vars = job.env_vars.len(),
total = env_map.len(),
"resolve_env: vars resolvidas (projeto [+ serviço] + overrides do job)"
);
Ok(env_map.into_iter().collect())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[tokio::test]
async fn secret_ausente_vira_erro_que_diz_qual_e_onde() {
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(crate::store::connect(dir.path()).await.unwrap());
let secrets = SecretsManager::new(&dir.path().join("master.key"), db).unwrap();
let err = secret_value(
&secrets,
"proj_1",
None,
"DB_PASSWORD",
"DATABASE_PASSWORD",
"serviço",
)
.await
.unwrap_err()
.to_string();
assert!(err.contains("DB_PASSWORD"), "{err}");
assert!(err.contains("DATABASE_PASSWORD"), "{err}");
assert!(err.contains("proj_1"), "{err}");
}
#[tokio::test]
async fn secret_existente_continua_resolvendo() {
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(crate::store::connect(dir.path()).await.unwrap());
db.seed_project_for_test("proj_1");
let secrets = SecretsManager::new(&dir.path().join("master.key"), db).unwrap();
secrets.set("proj_1", "TOKEN", "abc123").await.unwrap();
let v = secret_value(&secrets, "proj_1", None, "TOKEN", "API_TOKEN", "projeto")
.await
.unwrap();
assert_eq!(v, "abc123");
}
#[tokio::test]
async fn escopos_de_secret_na_resolucao_de_servico_e_job() {
use shared::{EnvVar, EnvVarValue};
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(crate::store::connect(dir.path()).await.unwrap());
db.seed_project_for_test("p1");
db.seed_service_for_test("p1", "s1");
let secrets = SecretsManager::new(&dir.path().join("master.key"), db.clone()).unwrap();
secrets.set("p1", "SENHA", "do-projeto").await.unwrap();
secrets.set("p1", "SO_PROJETO", "pp").await.unwrap();
secrets
.set_scoped("p1", Some("s1"), "SENHA", "do-servico")
.await
.unwrap();
secrets
.set_scoped("p1", Some("s1"), "SO_SERVICO", "ss")
.await
.unwrap();
let sec = |k: &str, n: &str| EnvVar {
key: k.into(),
value: EnvVarValue::Secret(n.into()),
};
db.infra()
.update(|st| {
st.projects[0].services[0].spec.env_vars = vec![
sec("A", "SENHA"),
sec("B", "SO_PROJETO"),
sec("C", "SO_SERVICO"),
];
Ok(())
})
.unwrap();
let svc = crate::store::services::get(&db, "s1")
.await
.unwrap()
.unwrap();
let map: std::collections::HashMap<_, _> = resolve(&db, &secrets, &svc)
.await
.unwrap()
.into_iter()
.collect();
assert_eq!(map["A"], "do-servico");
assert_eq!(map["B"], "pp");
assert_eq!(map["C"], "ss");
let mk_job = |trigger: Option<&str>| shared::Job {
id: "j".into(),
project_id: "p1".into(),
trigger_service_id: trigger.map(String::from),
name: "j".into(),
compose: String::new(),
git_source: None,
main_service: "m".into(),
env_vars: vec![sec("X", "SO_SERVICO")],
env_comments: vec![],
enabled: true,
recurrence: None,
last_run_at: None,
next_run_at: None,
created_at: chrono::Utc::now(),
};
let com: std::collections::HashMap<_, _> = resolve_job(&db, &secrets, &mk_job(Some("s1")))
.await
.unwrap()
.into_iter()
.collect();
assert_eq!(com["X"], "ss");
let e = resolve_job(&db, &secrets, &mk_job(None))
.await
.unwrap_err()
.to_string();
assert!(e.contains("SO_SERVICO") && e.contains("job"), "{e}");
}
}