greentic_operator/
secrets_setup.rs1use std::{
19 collections::HashMap,
20 path::{Path, PathBuf},
21};
22
23use anyhow::{Result, anyhow};
24use greentic_secrets_lib::core::Error as SecretError;
25use greentic_secrets_lib::{
26 ApplyOptions, DevStore, SecretFormat, SecretsStore, SeedDoc, SeedEntry, SeedValue, apply_seed,
27};
28use serde_yaml_bw;
29use tracing::{debug, info};
30
31use crate::{
32 dev_store_path, secret_requirements::load_secret_keys_from_pack,
33 secrets_gate::canonical_secret_uri,
34};
35
36pub fn resolve_env(override_env: Option<&str>) -> String {
37 override_env
38 .map(|value| value.to_string())
39 .or_else(|| std::env::var("GREENTIC_ENV").ok())
40 .unwrap_or_else(|| "dev".to_string())
41}
42
43pub struct SecretsSetup {
44 store: DevStore,
45 store_path: PathBuf,
46 env: String,
47 tenant: String,
48 team: Option<String>,
49 seeds: HashMap<String, SeedEntry>,
50}
51
52impl SecretsSetup {
53 pub fn new(bundle_root: &Path, env: &str, tenant: &str, team: Option<&str>) -> Result<Self> {
54 let store_path = dev_store_path::ensure_path(bundle_root)?;
55 info!(path = %store_path.display(), "secrets: using dev store backend");
56 let store = DevStore::with_path(&store_path).map_err(|err| {
57 anyhow!(
58 "failed to open dev secrets store {}: {err}",
59 store_path.display()
60 )
61 })?;
62 let seeds = load_seed_entries(bundle_root)?;
63 Ok(Self {
64 store,
65 store_path,
66 env: env.to_string(),
67 tenant: tenant.to_string(),
68 team: team.map(|value| value.to_string()),
69 seeds,
70 })
71 }
72
73 pub fn store_path(&self) -> &Path {
74 &self.store_path
75 }
76
77 pub async fn ensure_pack_secrets(&self, pack_path: &Path, provider_id: &str) -> Result<()> {
78 let keys = load_secret_keys_from_pack(pack_path)?;
79 if keys.is_empty() {
80 return Ok(());
81 }
82 let mut missing = Vec::new();
83 for key in keys {
84 let uri = canonical_secret_uri(
85 &self.env,
86 &self.tenant,
87 self.team.as_deref(),
88 provider_id,
89 &key,
90 );
91 debug!(uri = %uri, provider = %provider_id, key = %key, "canonicalized secret requirement");
92 match self.store.get(&uri).await {
93 Ok(_) => continue,
94 Err(SecretError::NotFound { .. }) => {
95 let source = if self.seeds.contains_key(&uri) {
96 "seeds.yaml"
97 } else {
98 "placeholder"
99 };
100 debug!(uri = %uri, source, "seeding missing secret");
101 missing.push(
102 self.seeds
103 .get(&uri)
104 .cloned()
105 .unwrap_or_else(|| placeholder_entry(uri.clone())),
106 );
107 }
108 Err(err) => {
109 return Err(anyhow!("failed to read secret {}: {err}", uri));
110 }
111 }
112 }
113 if missing.is_empty() {
114 return Ok(());
115 }
116 let report = apply_seed(
117 &self.store,
118 &SeedDoc { entries: missing },
119 ApplyOptions::default(),
120 )
121 .await;
122 if !report.failed.is_empty() {
123 return Err(anyhow!("failed to seed secrets: {:?}", report.failed));
124 }
125 Ok(())
126 }
127}
128
129fn load_seed_entries(bundle_root: &Path) -> Result<HashMap<String, SeedEntry>> {
130 for candidate in seed_paths(bundle_root) {
131 if candidate.exists() {
132 let contents = std::fs::read_to_string(&candidate)?;
133 let doc: SeedDoc = serde_yaml_bw::from_str(&contents)?;
134 return Ok(doc
135 .entries
136 .into_iter()
137 .map(|entry| (entry.uri.clone(), entry))
138 .collect());
139 }
140 }
141 Ok(HashMap::new())
142}
143
144fn seed_paths(bundle_root: &Path) -> [PathBuf; 2] {
145 [
146 bundle_root.join("seeds.yaml"),
147 bundle_root.join("state").join("seeds.yaml"),
148 ]
149}
150
151fn placeholder_entry(uri: String) -> SeedEntry {
152 SeedEntry {
153 uri: uri.clone(),
154 format: SecretFormat::Text,
155 value: SeedValue::Text {
156 text: format!("placeholder for {uri}"),
157 },
158 description: Some("auto-applied placeholder".to_string()),
159 }
160}