greentic_setup/
deployment_targets.rs1use std::path::{Path, PathBuf};
2
3use anyhow::Context;
4use dialoguer::{Confirm, Select};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Serialize, Deserialize)]
8pub struct DeploymentTargetsDocument {
9 pub version: String,
10 pub targets: Vec<DeploymentTargetRecord>,
11}
12
13#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
14pub struct DeploymentTargetRecord {
15 pub target: String,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub provider_pack: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub default: Option<bool>,
20}
21
22pub fn persist_explicit_deployment_targets(
23 bundle_root: &Path,
24 targets: &[DeploymentTargetRecord],
25) -> anyhow::Result<Option<PathBuf>> {
26 if targets.is_empty() {
27 return Ok(None);
28 }
29
30 let doc = DeploymentTargetsDocument {
31 version: "1".to_string(),
32 targets: targets.to_vec(),
33 };
34
35 let path = bundle_root
36 .join(".greentic")
37 .join("deployment-targets.json");
38 if let Some(parent) = path.parent() {
39 std::fs::create_dir_all(parent)?;
40 }
41 let payload = serde_json::to_string_pretty(&doc).context("serialize deployment targets")?;
42 std::fs::write(&path, payload)
43 .with_context(|| format!("failed to write {}", path.display()))?;
44 Ok(Some(path))
45}
46
47pub fn prompt_deployment_targets(
48 candidates: &[PathBuf],
49) -> anyhow::Result<Vec<DeploymentTargetRecord>> {
50 let mut targets = Vec::new();
51 for candidate in candidates {
52 let label = candidate.display().to_string();
53 let should_include = Confirm::new()
54 .with_prompt(format!(
55 "Use deployer pack {label} for gtc start deployment?"
56 ))
57 .default(true)
58 .interact()?;
59 if !should_include {
60 continue;
61 }
62 let choices = ["aws", "gcp", "azure", "single-vm"];
63 let index = Select::new()
64 .with_prompt(format!("Which deployment target does {label} implement?"))
65 .items(choices)
66 .default(0)
67 .interact()?;
68 targets.push(DeploymentTargetRecord {
69 target: choices[index].to_string(),
70 provider_pack: Some(label),
71 default: None,
72 });
73 }
74 if targets.len() == 1
75 && let Some(first) = targets.first_mut()
76 {
77 first.default = Some(true);
78 }
79 Ok(targets)
80}
81
82pub fn discover_deployer_pack_candidates(bundle_root: &Path) -> anyhow::Result<Vec<PathBuf>> {
83 let mut candidates = Vec::new();
84 for search_dir in [
85 bundle_root.join("packs"),
86 bundle_root.join("providers").join("deployer"),
87 ] {
88 if !search_dir.exists() {
89 continue;
90 }
91 for entry in std::fs::read_dir(&search_dir)? {
92 let entry = entry?;
93 let path = entry.path();
94 if path.extension().and_then(|ext| ext.to_str()) != Some("gtpack") {
95 continue;
96 }
97 let name = path
98 .file_name()
99 .and_then(|value| value.to_str())
100 .unwrap_or_default()
101 .to_ascii_lowercase();
102 if [
103 "terraform",
104 "aws",
105 "gcp",
106 "azure",
107 "single-vm",
108 "single_vm",
109 "helm",
110 "operator",
111 "serverless",
112 "snap",
113 "juju",
114 "k8s",
115 ]
116 .iter()
117 .any(|needle| name.contains(needle))
118 && let Ok(relative) = path.strip_prefix(bundle_root)
119 {
120 candidates.push(relative.to_path_buf());
121 }
122 }
123 }
124 candidates.sort();
125 candidates.dedup();
126 Ok(candidates)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn persists_explicit_targets() {
135 let temp = tempfile::tempdir().expect("tempdir");
136 let path = persist_explicit_deployment_targets(
137 temp.path(),
138 &[DeploymentTargetRecord {
139 target: "aws".into(),
140 provider_pack: Some("packs/terraform.gtpack".into()),
141 default: Some(true),
142 }],
143 )
144 .expect("persist")
145 .expect("path");
146 let written = std::fs::read_to_string(path).expect("read");
147 assert!(written.contains("\"target\": \"aws\""));
148 assert!(written.contains("\"provider_pack\": \"packs/terraform.gtpack\""));
149 assert!(written.contains("\"default\": true"));
150 }
151}