terraform_wrapper/
exec.rs1use std::process::Stdio;
2
3use tokio::process::Command as TokioCommand;
4use tracing::{debug, trace, warn};
5
6use crate::Terraform;
7use crate::error::{Error, Result};
8
9#[derive(Debug, Clone)]
11pub struct CommandOutput {
12 pub stdout: String,
14 pub stderr: String,
16 pub exit_code: i32,
18 pub success: bool,
20}
21
22impl CommandOutput {
23 #[must_use]
25 pub fn stdout_lines(&self) -> Vec<&str> {
26 self.stdout.lines().collect()
27 }
28}
29
30pub async fn run_terraform(tf: &Terraform, command_args: Vec<String>) -> Result<CommandOutput> {
41 run_terraform_inner(tf, command_args, &[0], tf.timeout).await
42}
43
44pub async fn run_terraform_allow_exit_codes(
49 tf: &Terraform,
50 command_args: Vec<String>,
51 allowed_codes: &[i32],
52) -> Result<CommandOutput> {
53 run_terraform_inner(tf, command_args, allowed_codes, tf.timeout).await
54}
55
56pub async fn run_terraform_with_timeout(
60 tf: &Terraform,
61 command_args: Vec<String>,
62 timeout: std::time::Duration,
63) -> Result<CommandOutput> {
64 run_terraform_inner(tf, command_args, &[0], Some(timeout)).await
65}
66
67async fn run_terraform_inner(
68 tf: &Terraform,
69 command_args: Vec<String>,
70 allowed_codes: &[i32],
71 timeout: Option<std::time::Duration>,
72) -> Result<CommandOutput> {
73 let mut cmd = TokioCommand::new(&tf.binary);
74
75 if let Some(ref working_dir) = tf.working_dir {
77 cmd.arg(format!("-chdir={}", working_dir.display()));
78 }
79
80 for arg in &command_args {
82 cmd.arg(arg);
83 }
84
85 for arg in &tf.global_args {
88 cmd.arg(arg);
89 }
90
91 for (key, value) in &tf.env {
93 cmd.env(key, value);
94 }
95
96 cmd.stdout(Stdio::piped());
97 cmd.stderr(Stdio::piped());
98
99 trace!(binary = ?tf.binary, args = ?command_args, timeout_secs = ?timeout.map(|t| t.as_secs()), "executing terraform command");
100
101 let io_result = if let Some(duration) = timeout {
102 match tokio::time::timeout(duration, cmd.output()).await {
103 Ok(result) => result,
104 Err(_) => {
105 warn!(
106 timeout_seconds = duration.as_secs(),
107 "terraform command timed out"
108 );
109 return Err(Error::Timeout {
110 timeout_seconds: duration.as_secs(),
111 });
112 }
113 }
114 } else {
115 cmd.output().await
116 };
117
118 let output = io_result.map_err(|e| {
119 if e.kind() == std::io::ErrorKind::NotFound {
120 Error::NotFound
121 } else {
122 Error::Io {
123 message: format!("failed to execute terraform: {e}"),
124 source: e,
125 }
126 }
127 })?;
128
129 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
130 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
131 let exit_code = output.status.code().unwrap_or(-1);
132 let success = allowed_codes.contains(&exit_code);
133
134 debug!(exit_code, success, "terraform command completed");
135 trace!(%stdout, "stdout");
136 if !stderr.is_empty() {
137 trace!(%stderr, "stderr");
138 }
139
140 if !success {
141 return Err(Error::CommandFailed {
142 command: command_args.first().cloned().unwrap_or_default(),
143 exit_code,
144 stdout,
145 stderr,
146 });
147 }
148
149 Ok(CommandOutput {
150 stdout,
151 stderr,
152 exit_code,
153 success,
154 })
155}