greentic_setup/
secrets.rs1use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9use anyhow::{Result, anyhow};
10use greentic_secrets_lib::core::Error as SecretError;
11use greentic_secrets_lib::{
12 ApplyOptions, DevStore, SecretFormat, SecretsStore, SeedDoc, SeedEntry, SeedValue, apply_seed,
13};
14use tracing::{debug, info};
15
16use crate::canonical_secret_uri;
17
18const STORE_RELATIVE: &str = ".greentic/dev/.dev.secrets.env";
21const STORE_STATE_RELATIVE: &str = ".greentic/state/dev/.dev.secrets.env";
22const OVERRIDE_ENV: &str = "GREENTIC_DEV_SECRETS_PATH";
23
24pub fn override_path() -> Option<PathBuf> {
26 std::env::var(OVERRIDE_ENV).ok().map(PathBuf::from)
27}
28
29pub fn find_existing(bundle_root: &Path) -> Option<PathBuf> {
31 find_existing_with_override(bundle_root, override_path().as_deref())
32}
33
34pub fn find_existing_with_override(
36 bundle_root: &Path,
37 override_path: Option<&Path>,
38) -> Option<PathBuf> {
39 if let Some(path) = override_path
40 && path.exists()
41 {
42 return Some(path.to_path_buf());
43 }
44 candidate_paths(bundle_root)
45 .into_iter()
46 .find(|candidate| candidate.exists())
47}
48
49pub fn ensure_path(bundle_root: &Path) -> Result<PathBuf> {
51 if let Some(path) = override_path() {
52 ensure_parent(&path)?;
53 return Ok(path);
54 }
55 let path = bundle_root.join(STORE_RELATIVE);
56 ensure_parent(&path)?;
57 Ok(path)
58}
59
60pub fn default_path(bundle_root: &Path) -> PathBuf {
62 bundle_root.join(STORE_RELATIVE)
63}
64
65fn candidate_paths(bundle_root: &Path) -> [PathBuf; 2] {
66 [
67 bundle_root.join(STORE_RELATIVE),
68 bundle_root.join(STORE_STATE_RELATIVE),
69 ]
70}
71
72fn ensure_parent(path: &Path) -> Result<()> {
73 if let Some(parent) = path.parent() {
74 std::fs::create_dir_all(parent)?;
75 }
76 Ok(())
77}
78
79pub struct SecretsSetup {
86 store: DevStore,
87 store_path: PathBuf,
88 env: String,
89 tenant: String,
90 team: Option<String>,
91 seeds: HashMap<String, SeedEntry>,
92}
93
94impl SecretsSetup {
95 pub fn new(bundle_root: &Path, env: &str, tenant: &str, team: Option<&str>) -> Result<Self> {
96 let store_path = ensure_path(bundle_root)?;
97 info!(path = %store_path.display(), "secrets: using dev store backend");
98 let store = DevStore::with_path(&store_path).map_err(|err| {
99 anyhow!(
100 "failed to open dev secrets store {}: {err}",
101 store_path.display()
102 )
103 })?;
104 let seeds = load_seed_entries(bundle_root)?;
105 Ok(Self {
106 store,
107 store_path,
108 env: env.to_string(),
109 tenant: tenant.to_string(),
110 team: team.map(|v| v.to_string()),
111 seeds,
112 })
113 }
114
115 pub fn store_path(&self) -> &Path {
117 &self.store_path
118 }
119
120 pub fn store(&self) -> &DevStore {
122 &self.store
123 }
124
125 pub async fn ensure_pack_secrets(&self, pack_path: &Path, provider_id: &str) -> Result<()> {
130 let keys = load_secret_keys_from_pack(pack_path)?;
131 if keys.is_empty() {
132 return Ok(());
133 }
134
135 let mut missing = Vec::new();
136 for key in keys {
137 let uri = canonical_secret_uri(
138 &self.env,
139 &self.tenant,
140 self.team.as_deref(),
141 provider_id,
142 &key,
143 );
144 debug!(uri = %uri, provider = %provider_id, key = %key, "canonicalized secret requirement");
145 match self.store.get(&uri).await {
146 Ok(_) => continue,
147 Err(SecretError::NotFound { .. }) => {
148 let source = if self.seeds.contains_key(&uri) {
149 "seeds.yaml"
150 } else {
151 "placeholder"
152 };
153 debug!(uri = %uri, source, "seeding missing secret");
154 missing.push(
155 self.seeds
156 .get(&uri)
157 .cloned()
158 .unwrap_or_else(|| placeholder_entry(uri)),
159 );
160 }
161 Err(err) => {
162 return Err(anyhow!("failed to read secret {uri}: {err}"));
163 }
164 }
165 }
166
167 if missing.is_empty() {
168 return Ok(());
169 }
170 let report = apply_seed(
171 &self.store,
172 &SeedDoc { entries: missing },
173 ApplyOptions::default(),
174 )
175 .await;
176 if !report.failed.is_empty() {
177 return Err(anyhow!("failed to seed secrets: {:?}", report.failed));
178 }
179 Ok(())
180 }
181}
182
183fn load_seed_entries(bundle_root: &Path) -> Result<HashMap<String, SeedEntry>> {
186 for candidate in seed_paths(bundle_root) {
187 if candidate.exists() {
188 let contents = std::fs::read_to_string(&candidate)?;
189 let doc: SeedDoc = serde_yaml_bw::from_str(&contents)?;
190 return Ok(doc
191 .entries
192 .into_iter()
193 .map(|entry| (entry.uri.clone(), entry))
194 .collect());
195 }
196 }
197 Ok(HashMap::new())
198}
199
200fn seed_paths(bundle_root: &Path) -> [PathBuf; 2] {
201 [
202 bundle_root.join("seeds.yaml"),
203 bundle_root.join("state").join("seeds.yaml"),
204 ]
205}
206
207fn placeholder_entry(uri: String) -> SeedEntry {
208 SeedEntry {
209 uri: uri.clone(),
210 format: SecretFormat::Text,
211 value: SeedValue::Text {
212 text: format!("placeholder for {uri}"),
213 },
214 description: Some("auto-applied placeholder".to_string()),
215 }
216}
217
218pub fn load_secret_keys_from_pack(pack_path: &Path) -> Result<Vec<String>> {
223 let file = std::fs::File::open(pack_path)?;
224 let mut archive = zip::ZipArchive::new(file)?;
225
226 for entry_name in &[
227 "assets/secret-requirements.json",
228 "assets/secret_requirements.json",
229 "secret-requirements.json",
230 "secret_requirements.json",
231 ] {
232 match archive.by_name(entry_name) {
233 Ok(reader) => {
234 let reqs: Vec<SecretRequirement> = serde_json::from_reader(reader)?;
235 return Ok(reqs.into_iter().map(|r| r.key).collect());
236 }
237 Err(zip::result::ZipError::FileNotFound) => continue,
238 Err(err) => return Err(err.into()),
239 }
240 }
241 Ok(vec![])
242}
243
244#[derive(serde::Deserialize)]
245struct SecretRequirement {
246 key: String,
247}
248
249pub fn open_dev_store(bundle_root: &Path) -> Result<DevStore> {
251 let store_path = ensure_path(bundle_root)?;
252 DevStore::with_path(&store_path).map_err(|err| {
253 anyhow!(
254 "failed to open dev secrets store {}: {err}",
255 store_path.display()
256 )
257 })
258}