terraform_wrapper/
exec.rs1use std::fmt;
2use std::process::Stdio;
3
4use tokio::process::Command as TokioCommand;
5use tracing::{debug, trace, warn};
6
7use crate::Terraform;
8use crate::error::{Error, Result};
9
10#[derive(Debug, Clone)]
12pub struct CommandOutput {
13 pub stdout: String,
15 pub stderr: String,
17 pub exit_code: i32,
19 pub success: bool,
21}
22
23impl CommandOutput {
24 #[must_use]
26 pub fn stdout_lines(&self) -> Vec<&str> {
27 self.stdout.lines().collect()
28 }
29}
30
31impl fmt::Display for CommandOutput {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 write!(f, "{}", self.stdout.trim())
34 }
35}
36
37pub async fn run_terraform(tf: &Terraform, command_args: Vec<String>) -> Result<CommandOutput> {
48 run_terraform_inner(tf, command_args, &[0], tf.timeout).await
49}
50
51pub async fn run_terraform_allow_exit_codes(
56 tf: &Terraform,
57 command_args: Vec<String>,
58 allowed_codes: &[i32],
59) -> Result<CommandOutput> {
60 run_terraform_inner(tf, command_args, allowed_codes, tf.timeout).await
61}
62
63pub async fn run_terraform_with_timeout(
67 tf: &Terraform,
68 command_args: Vec<String>,
69 timeout: std::time::Duration,
70) -> Result<CommandOutput> {
71 run_terraform_inner(tf, command_args, &[0], Some(timeout)).await
72}
73
74async fn run_terraform_inner(
75 tf: &Terraform,
76 command_args: Vec<String>,
77 allowed_codes: &[i32],
78 timeout: Option<std::time::Duration>,
79) -> Result<CommandOutput> {
80 let mut cmd = TokioCommand::new(&tf.binary);
81
82 if let Some(ref working_dir) = tf.working_dir {
84 cmd.arg(format!("-chdir={}", working_dir.display()));
85 }
86
87 for arg in &command_args {
89 cmd.arg(arg);
90 }
91
92 for arg in &tf.global_args {
95 cmd.arg(arg);
96 }
97
98 for (key, value) in &tf.env {
100 cmd.env(key, value);
101 }
102
103 cmd.stdout(Stdio::piped());
104 cmd.stderr(Stdio::piped());
105
106 trace!(binary = ?tf.binary, args = ?command_args, timeout_secs = ?timeout.map(|t| t.as_secs()), "executing terraform command");
107
108 let io_result = if let Some(duration) = timeout {
109 match tokio::time::timeout(duration, cmd.output()).await {
110 Ok(result) => result,
111 Err(_) => {
112 warn!(
113 timeout_seconds = duration.as_secs(),
114 "terraform command timed out"
115 );
116 return Err(Error::Timeout {
117 timeout_seconds: duration.as_secs(),
118 });
119 }
120 }
121 } else {
122 cmd.output().await
123 };
124
125 let output = io_result.map_err(|e| {
126 if e.kind() == std::io::ErrorKind::NotFound {
127 Error::NotFound
128 } else {
129 Error::Io {
130 message: format!("failed to execute terraform: {e}"),
131 source: e,
132 }
133 }
134 })?;
135
136 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
137 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
138 let exit_code = output.status.code().unwrap_or(-1);
139 let success = allowed_codes.contains(&exit_code);
140
141 debug!(exit_code, success, "terraform command completed");
142 trace!(%stdout, "stdout");
143 if !stderr.is_empty() {
144 trace!(%stderr, "stderr");
145 }
146
147 if !success {
148 return Err(Error::CommandFailed {
149 command: command_args.first().cloned().unwrap_or_default(),
150 exit_code,
151 stdout,
152 stderr,
153 });
154 }
155
156 Ok(CommandOutput {
157 stdout,
158 stderr,
159 exit_code,
160 success,
161 })
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn display_command_output_trims_whitespace() {
170 let output = CommandOutput {
171 stdout: " hello world \n".to_string(),
172 stderr: String::new(),
173 exit_code: 0,
174 success: true,
175 };
176 assert_eq!(output.to_string(), "hello world");
177 }
178
179 #[test]
180 fn display_command_output_empty() {
181 let output = CommandOutput {
182 stdout: String::new(),
183 stderr: String::new(),
184 exit_code: 0,
185 success: true,
186 };
187 assert_eq!(output.to_string(), "");
188 }
189
190 #[test]
191 fn display_command_output_multiline() {
192 let output = CommandOutput {
193 stdout: "line1\nline2\nline3\n".to_string(),
194 stderr: String::new(),
195 exit_code: 0,
196 success: true,
197 };
198 assert_eq!(output.to_string(), "line1\nline2\nline3");
199 }
200}