terraform_wrapper/commands/
plan.rs1use crate::Terraform;
2use crate::command::TerraformCommand;
3use crate::error::Result;
4use crate::exec::{self, CommandOutput};
5
6#[derive(Debug, Clone, Default)]
26pub struct PlanCommand {
27 vars: Vec<(String, String)>,
28 var_files: Vec<String>,
29 out: Option<String>,
30 targets: Vec<String>,
31 replace: Vec<String>,
32 destroy: bool,
33 refresh_only: bool,
34 refresh: Option<bool>,
35 compact_warnings: bool,
36 lock: Option<bool>,
37 lock_timeout: Option<String>,
38 parallelism: Option<u32>,
39 detailed_exitcode: bool,
40 json: bool,
41 raw_args: Vec<String>,
42}
43
44impl PlanCommand {
45 #[must_use]
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 #[must_use]
53 pub fn var(mut self, name: &str, value: &str) -> Self {
54 self.vars.push((name.to_string(), value.to_string()));
55 self
56 }
57
58 #[must_use]
60 pub fn var_file(mut self, path: &str) -> Self {
61 self.var_files.push(path.to_string());
62 self
63 }
64
65 #[must_use]
67 pub fn out(mut self, path: &str) -> Self {
68 self.out = Some(path.to_string());
69 self
70 }
71
72 #[must_use]
74 pub fn target(mut self, resource: &str) -> Self {
75 self.targets.push(resource.to_string());
76 self
77 }
78
79 #[must_use]
81 pub fn replace(mut self, resource: &str) -> Self {
82 self.replace.push(resource.to_string());
83 self
84 }
85
86 #[must_use]
88 pub fn destroy(mut self) -> Self {
89 self.destroy = true;
90 self
91 }
92
93 #[must_use]
95 pub fn refresh_only(mut self) -> Self {
96 self.refresh_only = true;
97 self
98 }
99
100 #[must_use]
104 pub fn refresh(mut self, enabled: bool) -> Self {
105 self.refresh = Some(enabled);
106 self
107 }
108
109 #[must_use]
111 pub fn compact_warnings(mut self) -> Self {
112 self.compact_warnings = true;
113 self
114 }
115
116 #[must_use]
118 pub fn lock(mut self, enabled: bool) -> Self {
119 self.lock = Some(enabled);
120 self
121 }
122
123 #[must_use]
125 pub fn lock_timeout(mut self, timeout: &str) -> Self {
126 self.lock_timeout = Some(timeout.to_string());
127 self
128 }
129
130 #[must_use]
132 pub fn parallelism(mut self, n: u32) -> Self {
133 self.parallelism = Some(n);
134 self
135 }
136
137 #[must_use]
143 pub fn detailed_exitcode(mut self) -> Self {
144 self.detailed_exitcode = true;
145 self
146 }
147
148 #[must_use]
153 pub fn json(mut self) -> Self {
154 self.json = true;
155 self
156 }
157
158 #[must_use]
160 pub fn arg(mut self, arg: impl Into<String>) -> Self {
161 self.raw_args.push(arg.into());
162 self
163 }
164}
165
166impl TerraformCommand for PlanCommand {
167 type Output = CommandOutput;
168
169 fn args(&self) -> Vec<String> {
170 let mut args = vec!["plan".to_string()];
171 for (name, value) in &self.vars {
172 args.push(format!("-var={name}={value}"));
173 }
174 for file in &self.var_files {
175 args.push(format!("-var-file={file}"));
176 }
177 if let Some(ref out) = self.out {
178 args.push(format!("-out={out}"));
179 }
180 for target in &self.targets {
181 args.push(format!("-target={target}"));
182 }
183 for resource in &self.replace {
184 args.push(format!("-replace={resource}"));
185 }
186 if self.destroy {
187 args.push("-destroy".to_string());
188 }
189 if self.refresh_only {
190 args.push("-refresh-only".to_string());
191 }
192 if let Some(refresh) = self.refresh {
193 args.push(format!("-refresh={refresh}"));
194 }
195 if self.compact_warnings {
196 args.push("-compact-warnings".to_string());
197 }
198 if let Some(lock) = self.lock {
199 args.push(format!("-lock={lock}"));
200 }
201 if let Some(ref timeout) = self.lock_timeout {
202 args.push(format!("-lock-timeout={timeout}"));
203 }
204 if let Some(n) = self.parallelism {
205 args.push(format!("-parallelism={n}"));
206 }
207 if self.detailed_exitcode {
208 args.push("-detailed-exitcode".to_string());
209 }
210 if self.json {
211 args.push("-json".to_string());
212 }
213 args.extend(self.raw_args.clone());
214 args
215 }
216
217 fn supports_input(&self) -> bool {
218 true
219 }
220
221 async fn execute(&self, tf: &Terraform) -> Result<CommandOutput> {
222 exec::run_terraform_allow_exit_codes(tf, self.prepare_args(tf), &[0, 2]).await
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn default_args() {
232 let cmd = PlanCommand::new();
233 assert_eq!(cmd.args(), vec!["plan"]);
234 }
235
236 #[test]
237 fn full_options() {
238 let cmd = PlanCommand::new()
239 .var("region", "us-west-2")
240 .var_file("prod.tfvars")
241 .out("tfplan")
242 .target("module.vpc")
243 .replace("aws_instance.web")
244 .destroy()
245 .refresh(false)
246 .compact_warnings()
247 .parallelism(10)
248 .json();
249 let args = cmd.args();
250 assert_eq!(args[0], "plan");
251 assert!(args.contains(&"-var=region=us-west-2".to_string()));
252 assert!(args.contains(&"-var-file=prod.tfvars".to_string()));
253 assert!(args.contains(&"-out=tfplan".to_string()));
254 assert!(args.contains(&"-target=module.vpc".to_string()));
255 assert!(args.contains(&"-replace=aws_instance.web".to_string()));
256 assert!(args.contains(&"-destroy".to_string()));
257 assert!(args.contains(&"-refresh=false".to_string()));
258 assert!(args.contains(&"-compact-warnings".to_string()));
259 assert!(args.contains(&"-parallelism=10".to_string()));
260 assert!(args.contains(&"-json".to_string()));
261 }
262
263 #[test]
264 fn multiple_targets() {
265 let cmd = PlanCommand::new().target("module.vpc").target("module.rds");
266 let args = cmd.args();
267 assert!(args.contains(&"-target=module.vpc".to_string()));
268 assert!(args.contains(&"-target=module.rds".to_string()));
269 }
270}