1use std::path::Path;
7
8use anyhow::{Context, anyhow};
9use serde_json::{Map as JsonMap, Value};
10
11use crate::plan::SetupPlan;
12use crate::platform_setup::load_effective_static_routes_defaults;
13use crate::{answers_crypto, discovery, setup_input};
14
15use super::plan_builders::infer_default_value;
16use super::types::{LoadedAnswers, SetupConfig};
17
18pub fn emit_answers(
23 config: &SetupConfig,
24 plan: &SetupPlan,
25 output_path: &Path,
26 key: Option<&str>,
27 interactive: bool,
28) -> anyhow::Result<()> {
29 let bundle = &plan.bundle;
30
31 let mut answers_doc = serde_json::json!({
33 "greentic_setup_version": "1.0.0",
34 "bundle_source": bundle.display().to_string(),
35 "tenant": config.tenant,
36 "team": config.team,
37 "env": config.env,
38 "platform_setup": {
39 "static_routes": plan.metadata.static_routes.to_answers(),
40 "deployment_targets": plan.metadata.deployment_targets
41 },
42 "setup_answers": {}
43 });
44
45 if !plan.metadata.static_routes.public_web_enabled
46 && plan.metadata.static_routes.public_base_url.is_none()
47 && let Some(existing) =
48 load_effective_static_routes_defaults(bundle, &config.tenant, config.team.as_deref())?
49 {
50 answers_doc["platform_setup"]["static_routes"] =
51 serde_json::to_value(existing.to_answers())?;
52 }
53
54 let setup_answers = answers_doc
56 .get_mut("setup_answers")
57 .and_then(|v| v.as_object_mut())
58 .ok_or_else(|| anyhow!("internal error: setup_answers not an object"))?;
59
60 for (provider_id, answers) in &plan.metadata.setup_answers {
62 setup_answers.insert(provider_id.clone(), answers.clone());
63 }
64
65 if bundle.exists() {
69 let discovered = discovery::discover(bundle)?;
70 for provider in discovered.providers {
71 let provider_id = provider.provider_id.clone();
72 let existing_is_empty = setup_answers
73 .get(&provider_id)
74 .and_then(|v| v.as_object())
75 .is_some_and(|m| m.is_empty());
76 if !setup_answers.contains_key(&provider_id) || existing_is_empty {
77 let template =
79 if let Some(spec) = setup_input::load_setup_spec(&provider.pack_path)? {
80 let mut entries = JsonMap::new();
82 for question in &spec.questions {
83 let default_value = infer_default_value(question);
84 entries.insert(question.name.clone(), default_value);
85 }
86 entries
87 } else {
88 JsonMap::new()
91 };
92 setup_answers.insert(provider_id, Value::Object(template));
93 }
94 }
95 }
96
97 if interactive {
99 prompt_secret_answers(bundle, &mut answers_doc)?;
100 }
101
102 encrypt_secret_answers(bundle, &mut answers_doc, key, interactive)?;
103
104 let output_content = serde_json::to_string_pretty(&answers_doc)
106 .context("failed to serialize answers document")?;
107
108 if let Some(parent) = output_path.parent() {
109 std::fs::create_dir_all(parent)
110 .with_context(|| format!("failed to create directory: {}", parent.display()))?;
111 }
112
113 std::fs::write(output_path, output_content)
114 .with_context(|| format!("failed to write answers to: {}", output_path.display()))?;
115
116 println!("Answers template written to: {}", output_path.display());
117 Ok(())
118}
119
120pub fn load_answers(
122 answers_path: &Path,
123 key: Option<&str>,
124 interactive: bool,
125) -> anyhow::Result<LoadedAnswers> {
126 let raw = setup_input::load_setup_input(answers_path)?;
127 let raw = if answers_crypto::has_encrypted_values(&raw) {
128 let resolved_key = match key {
129 Some(value) => value.to_string(),
130 None if interactive => answers_crypto::prompt_for_key("decrypting answers")?,
131 None => {
132 return Err(anyhow!(
133 "answers file contains encrypted secret values; rerun with --key or interactive input"
134 ));
135 }
136 };
137 answers_crypto::decrypt_tree(&raw, &resolved_key)?
138 } else {
139 raw
140 };
141 match raw {
142 Value::Object(map) => {
143 fn parse_optional_string(
144 map: &JsonMap<String, Value>,
145 key: &str,
146 ) -> anyhow::Result<Option<String>> {
147 match map.get(key) {
148 None | Some(Value::Null) => Ok(None),
149 Some(Value::String(value)) => Ok(Some(value.clone())),
150 Some(_) => Err(anyhow!("answers field '{key}' must be a string or null")),
151 }
152 }
153
154 let tenant = parse_optional_string(&map, "tenant")?;
155 let team = parse_optional_string(&map, "team")?;
156 let env = parse_optional_string(&map, "env")?;
157
158 let platform_setup = map
159 .get("platform_setup")
160 .cloned()
161 .map(serde_json::from_value)
162 .transpose()
163 .context("parse platform_setup answers")?
164 .unwrap_or_default();
165
166 if let Some(Value::Object(setup_answers)) = map.get("setup_answers") {
167 Ok(LoadedAnswers {
168 tenant,
169 team,
170 env,
171 platform_setup,
172 setup_answers: setup_answers.clone(),
173 })
174 } else if map.contains_key("bundle_source")
175 || map.contains_key("tenant")
176 || map.contains_key("team")
177 || map.contains_key("env")
178 || map.contains_key("platform_setup")
179 {
180 Ok(LoadedAnswers {
181 tenant,
182 team,
183 env,
184 platform_setup,
185 setup_answers: JsonMap::new(),
186 })
187 } else {
188 Ok(LoadedAnswers {
189 tenant,
190 team,
191 env,
192 platform_setup,
193 setup_answers: map,
194 })
195 }
196 }
197 _ => Err(anyhow!("answers file must be a JSON/YAML object")),
198 }
199}
200
201pub fn prompt_secret_answers(bundle: &Path, answers_doc: &mut Value) -> anyhow::Result<()> {
206 use rpassword::prompt_password;
207 use std::io::{self, Write as _};
208
209 let setup_answers = answers_doc
210 .get_mut("setup_answers")
211 .and_then(Value::as_object_mut)
212 .ok_or_else(|| anyhow!("internal error: setup_answers not an object"))?;
213
214 let discovered = if bundle.exists() {
215 discovery::discover(bundle)?
216 } else {
217 return Ok(());
218 };
219
220 let mut secret_questions: Vec<(String, String, String, bool)> = Vec::new(); for provider in &discovered.providers {
224 let Some(form_spec) =
225 crate::setup_to_formspec::pack_to_form_spec(&provider.pack_path, &provider.provider_id)
226 else {
227 continue;
228 };
229
230 let provider_answers = setup_answers
231 .get(&provider.provider_id)
232 .and_then(Value::as_object);
233
234 for question in form_spec.questions {
235 if !question.secret {
236 continue;
237 }
238
239 let has_value = provider_answers
241 .and_then(|m| m.get(&question.id))
242 .is_some_and(|v| !v.is_null() && v.as_str().map(|s| !s.is_empty()).unwrap_or(true));
243
244 if !has_value {
245 secret_questions.push((
246 provider.provider_id.clone(),
247 question.id.clone(),
248 question.title.clone(),
249 question.required,
250 ));
251 }
252 }
253 }
254
255 if secret_questions.is_empty() {
256 return Ok(());
257 }
258
259 println!();
260 println!("── Secret Values ──");
261 println!("Enter values for secret fields (input is hidden):");
262 println!("(Press Enter to skip optional fields)\n");
263
264 for (provider_id, field_id, title, required) in secret_questions {
265 let display_provider = crate::setup_to_formspec::strip_domain_prefix(&provider_id);
266 let marker = if required {
267 " (required)"
268 } else {
269 " (optional)"
270 };
271
272 print!(" [{display_provider}] {title}{marker}: ");
273 io::stdout().flush()?;
274
275 let input = prompt_password("").unwrap_or_default();
276 let trimmed = input.trim();
277
278 if !trimmed.is_empty() {
279 if let Some(provider_answers) = setup_answers
281 .get_mut(&provider_id)
282 .and_then(Value::as_object_mut)
283 {
284 provider_answers.insert(field_id, Value::String(trimmed.to_string()));
285 }
286 } else if required {
287 println!(" \x1b[33m⚠ Skipped (will need to be filled in later)\x1b[0m");
288 }
289 }
290
291 println!();
292 Ok(())
293}
294
295pub fn encrypt_secret_answers(
297 bundle: &Path,
298 answers_doc: &mut Value,
299 key: Option<&str>,
300 interactive: bool,
301) -> anyhow::Result<()> {
302 let setup_answers = answers_doc
303 .get_mut("setup_answers")
304 .and_then(Value::as_object_mut)
305 .ok_or_else(|| anyhow!("internal error: setup_answers not an object"))?;
306 let discovered = if bundle.exists() {
307 discovery::discover(bundle)?
308 } else {
309 return Ok(());
310 };
311
312 let mut secret_paths = Vec::new();
313 for provider in discovered.providers {
314 let Some(form_spec) =
315 crate::setup_to_formspec::pack_to_form_spec(&provider.pack_path, &provider.provider_id)
316 else {
317 continue;
318 };
319 let Some(provider_answers) = setup_answers
320 .get_mut(&provider.provider_id)
321 .and_then(Value::as_object_mut)
322 else {
323 continue;
324 };
325 for question in form_spec.questions {
326 if !question.secret {
327 continue;
328 }
329 let Some(value) = provider_answers.get(&question.id).cloned() else {
330 continue;
331 };
332 if value.is_null() || value == Value::String(String::new()) {
333 continue;
334 }
335 secret_paths.push((provider.provider_id.clone(), question.id.clone(), value));
336 }
337 }
338
339 if secret_paths.is_empty() {
340 return Ok(());
341 }
342
343 let resolved_key = match key {
344 Some(value) => value.to_string(),
345 None if interactive => answers_crypto::prompt_for_key("encrypting answers")?,
346 None => {
347 return Err(anyhow!(
348 "answer document includes secret values; rerun with --key or interactive input"
349 ));
350 }
351 };
352
353 for (provider_id, field_id, value) in secret_paths {
354 let encrypted = answers_crypto::encrypt_value(&value, &resolved_key)?;
355 if let Some(provider_answers) = setup_answers
356 .get_mut(&provider_id)
357 .and_then(Value::as_object_mut)
358 {
359 provider_answers.insert(field_id, encrypted);
360 }
361 }
362
363 Ok(())
364}