Skip to main content

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}