dev_prune/adapters/
terraform.rs1use super::{BloatDir, EnforcePolicy, PackageManager, dir_size, run_command_with_timeout};
32use anyhow::{Result, anyhow};
33use std::fs;
34use std::path::Path;
35
36pub struct Terraform;
38
39impl PackageManager for Terraform {
40 fn name(&self) -> &'static str {
41 "terraform"
42 }
43
44 fn detect(&self, path: &Path) -> bool {
45 let Ok(entries) = fs::read_dir(path) else {
49 return false;
50 };
51 entries.filter_map(Result::ok).any(|entry| {
52 let name = entry.file_name();
53 let Some(name) = name.to_str() else {
54 return false;
55 };
56 name.ends_with(".tf") || name.ends_with(".tf.json")
57 })
58 }
59
60 fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
61 let providers = path.join(".terraform").join("providers");
62 if !providers.is_dir() {
63 return Vec::new();
64 }
65 vec![BloatDir {
66 name: ".terraform/providers".to_string(),
67 path: providers.clone(),
68 size_bytes: dir_size(&providers),
69 shared_bytes: 0,
70 }]
71 }
72
73 fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
74 let lock = path.join(".terraform.lock.hcl");
75 let content = fs::read_to_string(&lock).map_err(|e| {
76 anyhow!(
77 "`.terraform.lock.hcl` could not be read ({e}) — without it `terraform \
78 init` selects provider versions afresh instead of restoring the ones \
79 being deleted."
80 )
81 })?;
82 if !content.contains("provider \"") {
86 return Err(anyhow!(
87 "`.terraform.lock.hcl` records no providers — it cannot prove \
88 `.terraform/providers` is rebuildable. Run `terraform init` and prune \
89 again."
90 ));
91 }
92 Ok(())
93 }
94
95 fn restore(&self, path: &Path, timeout: std::time::Duration) -> Result<()> {
96 run_command_with_timeout("terraform", &["init", "-backend=false"], path, timeout)
101 }
102
103 fn lockfiles(&self) -> &'static [&'static str] {
104 &[".terraform.lock.hcl"]
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use tempfile::tempdir;
112
113 const LOCK: &str =
114 "provider \"registry.terraform.io/hashicorp/aws\" {\n version = \"5.0.0\"\n}\n";
115
116 #[test]
117 fn detects_on_any_terraform_source_file() {
118 let dir = tempdir().unwrap();
119 assert!(!Terraform.detect(dir.path()));
120 fs::write(dir.path().join("README.md"), "not terraform").unwrap();
121 assert!(!Terraform.detect(dir.path()));
122 fs::write(
123 dir.path().join("main.tf"),
124 "resource \"null_resource\" \"a\" {}",
125 )
126 .unwrap();
127 assert!(Terraform.detect(dir.path()));
128 }
129
130 #[test]
131 fn detects_generated_json_configuration_too() {
132 let dir = tempdir().unwrap();
133 fs::write(dir.path().join("main.tf.json"), "{}").unwrap();
134 assert!(Terraform.detect(dir.path()));
135 }
136
137 #[test]
138 fn claims_the_provider_cache_and_nothing_else_under_dot_terraform() {
139 let dir = tempdir().unwrap();
140 let dot = dir.path().join(".terraform");
141 fs::create_dir_all(dot.join("providers")).unwrap();
142 fs::create_dir_all(dot.join("modules")).unwrap();
143 fs::write(dot.join("environment"), "production").unwrap();
144 fs::write(dot.join("terraform.tfstate"), "{}").unwrap();
145
146 let dirs = Terraform.bloat_dirs(dir.path());
147 assert_eq!(dirs.len(), 1);
148 assert_eq!(dirs[0].name, ".terraform/providers");
149 assert_eq!(dirs[0].path, dot.join("providers"));
150 }
151
152 #[test]
153 fn an_uninitialised_project_offers_nothing_to_prune() {
154 let dir = tempdir().unwrap();
155 fs::write(dir.path().join("main.tf"), "").unwrap();
156 assert!(Terraform.bloat_dirs(dir.path()).is_empty());
157 }
158
159 #[test]
160 fn a_missing_or_providerless_lockfile_is_refused() {
161 let dir = tempdir().unwrap();
162 assert!(
163 Terraform
164 .enforce_lockfile(dir.path(), EnforcePolicy::default())
165 .is_err()
166 );
167 fs::write(
168 dir.path().join(".terraform.lock.hcl"),
169 "# This file is maintained automatically by \"terraform init\".\n",
170 )
171 .unwrap();
172 assert!(
173 Terraform
174 .enforce_lockfile(dir.path(), EnforcePolicy::default())
175 .is_err()
176 );
177 fs::write(dir.path().join(".terraform.lock.hcl"), LOCK).unwrap();
178 assert!(
179 Terraform
180 .enforce_lockfile(dir.path(), EnforcePolicy::default())
181 .is_ok()
182 );
183 }
184}