terraform_wrapper/lib.rs
1//! A type-safe Terraform CLI wrapper for Rust.
2//!
3//! `terraform-wrapper` provides builder-pattern command structs for driving the
4//! Terraform CLI programmatically. Each command produces typed output and runs
5//! asynchronously via tokio.
6//!
7//! # Quick Start
8//!
9//! ```no_run
10//! use terraform_wrapper::{Terraform, TerraformCommand};
11//! use terraform_wrapper::commands::init::InitCommand;
12//! use terraform_wrapper::commands::apply::ApplyCommand;
13//! use terraform_wrapper::commands::output::{OutputCommand, OutputResult};
14//!
15//! # async fn example() -> terraform_wrapper::error::Result<()> {
16//! let tf = Terraform::builder()
17//! .working_dir("/tmp/my-infra")
18//! .build()?;
19//!
20//! InitCommand::new().execute(&tf).await?;
21//!
22//! ApplyCommand::new()
23//! .auto_approve()
24//! .var("region", "us-west-2")
25//! .execute(&tf)
26//! .await?;
27//!
28//! let result = OutputCommand::new()
29//! .name("public_ip")
30//! .raw()
31//! .execute(&tf)
32//! .await?;
33//! # Ok(())
34//! # }
35//! ```
36
37use std::collections::HashMap;
38use std::path::{Path, PathBuf};
39
40pub mod command;
41pub mod commands;
42pub mod error;
43pub mod exec;
44#[cfg(feature = "json")]
45pub mod types;
46
47pub use command::TerraformCommand;
48pub use error::{Error, Result};
49pub use exec::CommandOutput;
50
51/// Terraform client configuration.
52///
53/// Holds the binary path, working directory, environment variables, and global
54/// arguments shared across all command executions. Construct via
55/// [`Terraform::builder()`].
56#[derive(Debug, Clone)]
57pub struct Terraform {
58 pub(crate) binary: PathBuf,
59 pub(crate) working_dir: Option<PathBuf>,
60 pub(crate) env: HashMap<String, String>,
61 /// Args applied to every subcommand (e.g., `-no-color`).
62 pub(crate) global_args: Vec<String>,
63 /// Whether to add `-input=false` to commands that support it.
64 pub(crate) no_input: bool,
65}
66
67impl Terraform {
68 /// Create a new [`TerraformBuilder`].
69 #[must_use]
70 pub fn builder() -> TerraformBuilder {
71 TerraformBuilder::new()
72 }
73
74 /// Verify terraform is installed and return version info.
75 #[cfg(feature = "json")]
76 pub async fn version(&self) -> Result<types::version::VersionInfo> {
77 commands::version::VersionCommand::new().execute(self).await
78 }
79}
80
81/// Builder for constructing a [`Terraform`] client.
82///
83/// Defaults:
84/// - Binary: resolved via `TERRAFORM_PATH` env var, or `terraform` on `PATH`
85/// - `-no-color` enabled (disable with `.color(true)`)
86/// - `-input=false` enabled (disable with `.input(true)`)
87#[derive(Debug)]
88pub struct TerraformBuilder {
89 binary: Option<PathBuf>,
90 working_dir: Option<PathBuf>,
91 env: HashMap<String, String>,
92 no_color: bool,
93 input: bool,
94}
95
96impl TerraformBuilder {
97 fn new() -> Self {
98 Self {
99 binary: None,
100 working_dir: None,
101 env: HashMap::new(),
102 no_color: true,
103 input: false,
104 }
105 }
106
107 /// Set an explicit path to the terraform binary.
108 #[must_use]
109 pub fn binary(mut self, path: impl Into<PathBuf>) -> Self {
110 self.binary = Some(path.into());
111 self
112 }
113
114 /// Set the default working directory for all commands.
115 ///
116 /// This is passed as `-chdir=<path>` to terraform.
117 #[must_use]
118 pub fn working_dir(mut self, path: impl AsRef<Path>) -> Self {
119 self.working_dir = Some(path.as_ref().to_path_buf());
120 self
121 }
122
123 /// Set an environment variable for all terraform subprocesses.
124 #[must_use]
125 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
126 self.env.insert(key.into(), value.into());
127 self
128 }
129
130 /// Set a Terraform variable via environment (`TF_VAR_<name>`).
131 #[must_use]
132 pub fn env_var(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
133 self.env
134 .insert(format!("TF_VAR_{}", name.into()), value.into());
135 self
136 }
137
138 /// Enable or disable color output (default: disabled for programmatic use).
139 #[must_use]
140 pub fn color(mut self, enable: bool) -> Self {
141 self.no_color = !enable;
142 self
143 }
144
145 /// Enable or disable interactive input prompts (default: disabled).
146 #[must_use]
147 pub fn input(mut self, enable: bool) -> Self {
148 self.input = enable;
149 self
150 }
151
152 /// Build the [`Terraform`] client.
153 ///
154 /// Resolves the terraform binary in this order:
155 /// 1. Explicit path set via [`.binary()`](TerraformBuilder::binary)
156 /// 2. `TERRAFORM_PATH` environment variable
157 /// 3. `terraform` found on `PATH`
158 ///
159 /// Returns [`Error::NotFound`] if the binary cannot be located.
160 pub fn build(self) -> Result<Terraform> {
161 let binary = if let Some(path) = self.binary {
162 path
163 } else if let Ok(path) = std::env::var("TERRAFORM_PATH") {
164 PathBuf::from(path)
165 } else {
166 which::which("terraform").map_err(|_| Error::NotFound)?
167 };
168
169 let mut global_args = Vec::new();
170 if self.no_color {
171 global_args.push("-no-color".to_string());
172 }
173
174 Ok(Terraform {
175 binary,
176 working_dir: self.working_dir,
177 env: self.env,
178 global_args,
179 no_input: !self.input,
180 })
181 }
182}