Skip to main content

terraform_wrapper/commands/
apply.rs

1use crate::Terraform;
2use crate::command::TerraformCommand;
3use crate::error::Result;
4use crate::exec::{self, CommandOutput};
5
6/// Command for applying Terraform changes.
7///
8/// When applying a saved plan file, options like `-var`, `-var-file`, and
9/// `-target` are not valid (Terraform will reject them).
10///
11/// ```no_run
12/// # async fn example() -> terraform_wrapper::error::Result<()> {
13/// use terraform_wrapper::{Terraform, TerraformCommand};
14/// use terraform_wrapper::commands::apply::ApplyCommand;
15///
16/// let tf = Terraform::builder().working_dir("/tmp/infra").build()?;
17/// ApplyCommand::new()
18///     .auto_approve()
19///     .var("region", "us-west-2")
20///     .execute(&tf)
21///     .await?;
22/// # Ok(())
23/// # }
24/// ```
25#[derive(Debug, Clone, Default)]
26pub struct ApplyCommand {
27    plan_file: Option<String>,
28    auto_approve: bool,
29    vars: Vec<(String, String)>,
30    var_files: Vec<String>,
31    targets: Vec<String>,
32    replace: Vec<String>,
33    lock: Option<bool>,
34    lock_timeout: Option<String>,
35    parallelism: Option<u32>,
36    json: bool,
37    raw_args: Vec<String>,
38}
39
40impl ApplyCommand {
41    /// Create a new apply command with default options.
42    #[must_use]
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Apply a previously saved plan file (positional argument).
48    #[must_use]
49    pub fn plan_file(mut self, path: &str) -> Self {
50        self.plan_file = Some(path.to_string());
51        self
52    }
53
54    /// Skip interactive approval (`-auto-approve`).
55    #[must_use]
56    pub fn auto_approve(mut self) -> Self {
57        self.auto_approve = true;
58        self
59    }
60
61    /// Set a variable value (`-var="name=value"`).
62    #[must_use]
63    pub fn var(mut self, name: &str, value: &str) -> Self {
64        self.vars.push((name.to_string(), value.to_string()));
65        self
66    }
67
68    /// Add a variable definitions file (`-var-file`).
69    #[must_use]
70    pub fn var_file(mut self, path: &str) -> Self {
71        self.var_files.push(path.to_string());
72        self
73    }
74
75    /// Target a specific resource or module (`-target`).
76    #[must_use]
77    pub fn target(mut self, resource: &str) -> Self {
78        self.targets.push(resource.to_string());
79        self
80    }
81
82    /// Mark a resource for replacement (`-replace`).
83    #[must_use]
84    pub fn replace(mut self, resource: &str) -> Self {
85        self.replace.push(resource.to_string());
86        self
87    }
88
89    /// Enable or disable state locking (`-lock`).
90    #[must_use]
91    pub fn lock(mut self, enabled: bool) -> Self {
92        self.lock = Some(enabled);
93        self
94    }
95
96    /// Duration to wait for state lock (`-lock-timeout`).
97    #[must_use]
98    pub fn lock_timeout(mut self, timeout: &str) -> Self {
99        self.lock_timeout = Some(timeout.to_string());
100        self
101    }
102
103    /// Limit the number of concurrent operations (`-parallelism`).
104    #[must_use]
105    pub fn parallelism(mut self, n: u32) -> Self {
106        self.parallelism = Some(n);
107        self
108    }
109
110    /// Enable machine-readable JSON output (`-json`).
111    #[must_use]
112    pub fn json(mut self) -> Self {
113        self.json = true;
114        self
115    }
116
117    /// Add a raw argument (escape hatch for unsupported options).
118    #[must_use]
119    pub fn arg(mut self, arg: impl Into<String>) -> Self {
120        self.raw_args.push(arg.into());
121        self
122    }
123}
124
125impl TerraformCommand for ApplyCommand {
126    type Output = CommandOutput;
127
128    fn args(&self) -> Vec<String> {
129        let mut args = vec!["apply".to_string()];
130        if self.auto_approve {
131            args.push("-auto-approve".to_string());
132        }
133        for (name, value) in &self.vars {
134            args.push(format!("-var={name}={value}"));
135        }
136        for file in &self.var_files {
137            args.push(format!("-var-file={file}"));
138        }
139        for target in &self.targets {
140            args.push(format!("-target={target}"));
141        }
142        for resource in &self.replace {
143            args.push(format!("-replace={resource}"));
144        }
145        if let Some(lock) = self.lock {
146            args.push(format!("-lock={lock}"));
147        }
148        if let Some(ref timeout) = self.lock_timeout {
149            args.push(format!("-lock-timeout={timeout}"));
150        }
151        if let Some(n) = self.parallelism {
152            args.push(format!("-parallelism={n}"));
153        }
154        if self.json {
155            args.push("-json".to_string());
156        }
157        args.extend(self.raw_args.clone());
158        // Plan file is a positional argument at the end
159        if let Some(ref plan) = self.plan_file {
160            args.push(plan.clone());
161        }
162        args
163    }
164
165    fn supports_input(&self) -> bool {
166        true
167    }
168
169    async fn execute(&self, tf: &Terraform) -> Result<CommandOutput> {
170        exec::run_terraform(tf, self.prepare_args(tf)).await
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn default_args() {
180        let cmd = ApplyCommand::new();
181        assert_eq!(cmd.args(), vec!["apply"]);
182    }
183
184    #[test]
185    fn auto_approve_with_vars() {
186        let cmd = ApplyCommand::new()
187            .auto_approve()
188            .var("region", "us-west-2")
189            .var_file("prod.tfvars");
190        let args = cmd.args();
191        assert_eq!(args[0], "apply");
192        assert!(args.contains(&"-auto-approve".to_string()));
193        assert!(args.contains(&"-var=region=us-west-2".to_string()));
194        assert!(args.contains(&"-var-file=prod.tfvars".to_string()));
195    }
196
197    #[test]
198    fn plan_file_at_end() {
199        let cmd = ApplyCommand::new().auto_approve().plan_file("tfplan");
200        let args = cmd.args();
201        assert_eq!(args.last().unwrap(), "tfplan");
202    }
203
204    #[test]
205    fn parallelism_and_json() {
206        let cmd = ApplyCommand::new().parallelism(10).json();
207        let args = cmd.args();
208        assert!(args.contains(&"-parallelism=10".to_string()));
209        assert!(args.contains(&"-json".to_string()));
210    }
211}