1use anyhow::{Context, Result, bail};
8use serde_json::{self, Value};
9use std::{
10 collections::HashMap,
11 env,
12 fs,
13 path::{Path, PathBuf},
14 process::{Command, Stdio},
15};
16use sha2::{Digest, Sha256};
17
18fn ensure_terraform_installed() -> Result<()> {
19 let status = Command::new("terraform")
20 .arg("-version")
21 .stdout(Stdio::null())
22 .stderr(Stdio::null())
23 .status()
24 .context("Failed to execute `terraform -version`")?;
25 if !status.success() {
26 bail!("Terraform must be installed and in PATH");
27 }
28 Ok(())
29}
30
31fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
33 fs::create_dir_all(dst).with_context(|| format!("Failed to create directory {:?}", dst))?;
34 for entry in fs::read_dir(src).with_context(|| format!("Failed to read directory {:?}", src))? {
35 let entry = entry.with_context(|| format!("Failed to access entry in {:?}", src))?;
36 let path = entry.path();
37 let dest = dst.join(entry.file_name());
38 if path.is_dir() {
39 copy_dir_recursive(&path, &dest)?;
40 } else {
41 fs::copy(&path, &dest)
42 .with_context(|| format!("Failed to copy file {:?} to {:?}", path, dest))?;
43 }
44 }
45 Ok(())
46}
47
48fn prepare_work_dir(src_dir: &Path) -> Result<PathBuf> {
50 let mut hasher = Sha256::new();
51 hasher.update(src_dir.to_string_lossy().as_bytes());
52 let hash = format!("{:x}", hasher.finalize());
53 let work = env::temp_dir().join("atar").join(hash);
54 if !work.exists() {
55 println!("Copying Terraform files to temporary directory {}", work.display());
56 copy_dir_recursive(src_dir, &work)?;
57 }
58 Ok(work)
59}
60
61pub fn deploy<P: AsRef<Path>>(
65 file: P,
66 vars: &HashMap<String, String>,
67 debug: bool,
68) -> Result<HashMap<String, String>> {
69 ensure_terraform_installed()?;
70 let file = file
71 .as_ref()
72 .canonicalize()
73 .context("Failed to canonicalize Terraform path")?;
74 let src_dir = file
75 .parent()
76 .context("Cannot determine Terraform directory")?;
77 let work_dir = prepare_work_dir(src_dir)?;
78
79 println!("Initializing Terraform...");
81
82 let mut init = Command::new("terraform");
83 init.current_dir(&work_dir).arg("init");
84 if !debug {
85 init.stdout(Stdio::null()).stderr(Stdio::null());
86 }
87 let status = init
88 .status()
89 .context("Failed to execute `terraform init`")?;
90 if !status.success() {
91 bail!("`terraform init` failed with exit code {}", status);
92 }
93
94 println!("Applying Terraform...");
95 {
96 let mut cmd = Command::new("terraform");
97 cmd.current_dir(&work_dir).arg("apply").arg("-auto-approve");
98 for (k, v) in vars {
99 cmd.arg("-var").arg(format!("{}={}", k, v));
100 }
101 if !debug {
102 cmd.stdout(Stdio::null()).stderr(Stdio::null());
103 }
104 let status = cmd
105 .status()
106 .context("Failed to execute `terraform apply`")?;
107 if !status.success() {
108 bail!("`terraform apply` failed with exit code {}", status);
109 }
110 }
111
112 let output = Command::new("terraform")
114 .current_dir(&work_dir)
115 .arg("output")
116 .arg("-json")
117 .output()
118 .context("Failed to execute `terraform output -json`")?;
119 if !output.status.success() {
120 bail!(
121 "`terraform output -json` failed with exit code {}",
122 output.status
123 );
124 }
125 let raw: HashMap<String, Value> = serde_json::from_slice(&output.stdout)
126 .context("Failed to parse Terraform output JSON")?;
127 let mut results = HashMap::new();
128 for (key, val) in raw {
129 if let Some(inner) = val.get("value") {
130 let s = if inner.is_string() {
131 inner.as_str().unwrap().to_string()
132 } else {
133 inner.to_string()
134 };
135 results.insert(key, s);
136 }
137 }
138 Ok(results)
139}
140
141pub fn undeploy<P: AsRef<Path>>(
143 file: P,
144 vars: &HashMap<String, String>,
145 debug: bool,
146) -> Result<()> {
147 ensure_terraform_installed()?;
148 let file = file
149 .as_ref()
150 .canonicalize()
151 .context("Failed to canonicalize Terraform path")?;
152 let src_dir = file
153 .parent()
154 .context("Cannot determine Terraform directory")?;
155 let work_dir = prepare_work_dir(src_dir)?;
156
157 println!("Destroying Terraform...");
158
159 let mut cmd = Command::new("terraform");
160 cmd.current_dir(&work_dir).arg("destroy").arg("-auto-approve");
161 for (k, v) in vars {
162 cmd.arg("-var").arg(format!("{}={}", k, v));
163 }
164 if !debug {
165 cmd.stdout(Stdio::null()).stderr(Stdio::null());
166 }
167 let status = cmd
168 .status()
169 .context("Failed to execute `terraform destroy`")?;
170 if !status.success() {
171 bail!("`terraform destroy` failed with exit code {}", status);
172 }
173 println!("All resources have been destroyed.");
174 Ok(())
175}