use super::{BloatDir, EnforcePolicy, PackageManager, dir_size, run_command_with_timeout};
use anyhow::{Result, anyhow};
use std::fs;
use std::path::Path;
pub struct Terraform;
impl PackageManager for Terraform {
fn name(&self) -> &'static str {
"terraform"
}
fn detect(&self, path: &Path) -> bool {
let Ok(entries) = fs::read_dir(path) else {
return false;
};
entries.filter_map(Result::ok).any(|entry| {
let name = entry.file_name();
let Some(name) = name.to_str() else {
return false;
};
name.ends_with(".tf") || name.ends_with(".tf.json")
})
}
fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
let providers = path.join(".terraform").join("providers");
if !providers.is_dir() {
return Vec::new();
}
vec![BloatDir {
name: ".terraform/providers".to_string(),
path: providers.clone(),
size_bytes: dir_size(&providers),
shared_bytes: 0,
}]
}
fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
let lock = path.join(".terraform.lock.hcl");
let content = fs::read_to_string(&lock).map_err(|e| {
anyhow!(
"`.terraform.lock.hcl` could not be read ({e}) — without it `terraform \
init` selects provider versions afresh instead of restoring the ones \
being deleted."
)
})?;
if !content.contains("provider \"") {
return Err(anyhow!(
"`.terraform.lock.hcl` records no providers — it cannot prove \
`.terraform/providers` is rebuildable. Run `terraform init` and prune \
again."
));
}
Ok(())
}
fn restore(&self, path: &Path, timeout: std::time::Duration) -> Result<()> {
run_command_with_timeout("terraform", &["init", "-backend=false"], path, timeout)
}
fn lockfiles(&self) -> &'static [&'static str] {
&[".terraform.lock.hcl"]
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
const LOCK: &str =
"provider \"registry.terraform.io/hashicorp/aws\" {\n version = \"5.0.0\"\n}\n";
#[test]
fn detects_on_any_terraform_source_file() {
let dir = tempdir().unwrap();
assert!(!Terraform.detect(dir.path()));
fs::write(dir.path().join("README.md"), "not terraform").unwrap();
assert!(!Terraform.detect(dir.path()));
fs::write(
dir.path().join("main.tf"),
"resource \"null_resource\" \"a\" {}",
)
.unwrap();
assert!(Terraform.detect(dir.path()));
}
#[test]
fn detects_generated_json_configuration_too() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("main.tf.json"), "{}").unwrap();
assert!(Terraform.detect(dir.path()));
}
#[test]
fn claims_the_provider_cache_and_nothing_else_under_dot_terraform() {
let dir = tempdir().unwrap();
let dot = dir.path().join(".terraform");
fs::create_dir_all(dot.join("providers")).unwrap();
fs::create_dir_all(dot.join("modules")).unwrap();
fs::write(dot.join("environment"), "production").unwrap();
fs::write(dot.join("terraform.tfstate"), "{}").unwrap();
let dirs = Terraform.bloat_dirs(dir.path());
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, ".terraform/providers");
assert_eq!(dirs[0].path, dot.join("providers"));
}
#[test]
fn an_uninitialised_project_offers_nothing_to_prune() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("main.tf"), "").unwrap();
assert!(Terraform.bloat_dirs(dir.path()).is_empty());
}
#[test]
fn a_missing_or_providerless_lockfile_is_refused() {
let dir = tempdir().unwrap();
assert!(
Terraform
.enforce_lockfile(dir.path(), EnforcePolicy::default())
.is_err()
);
fs::write(
dir.path().join(".terraform.lock.hcl"),
"# This file is maintained automatically by \"terraform init\".\n",
)
.unwrap();
assert!(
Terraform
.enforce_lockfile(dir.path(), EnforcePolicy::default())
.is_err()
);
fs::write(dir.path().join(".terraform.lock.hcl"), LOCK).unwrap();
assert!(
Terraform
.enforce_lockfile(dir.path(), EnforcePolicy::default())
.is_ok()
);
}
}