Skip to main content

greentic_setup/
deployment_targets.rs

1use 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}