terraform_wrapper/
exec.rs1use std::process::Stdio;
2
3use tokio::process::Command as TokioCommand;
4use tracing::{debug, trace};
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> {
39 run_terraform_inner(tf, command_args, &[0]).await
40}
41
42pub async fn run_terraform_allow_exit_codes(
47 tf: &Terraform,
48 command_args: Vec<String>,
49 allowed_codes: &[i32],
50) -> Result<CommandOutput> {
51 run_terraform_inner(tf, command_args, allowed_codes).await
52}
53
54async fn run_terraform_inner(
55 tf: &Terraform,
56 command_args: Vec<String>,
57 allowed_codes: &[i32],
58) -> Result<CommandOutput> {
59 let mut cmd = TokioCommand::new(&tf.binary);
60
61 if let Some(ref working_dir) = tf.working_dir {
63 cmd.arg(format!("-chdir={}", working_dir.display()));
64 }
65
66 for arg in &command_args {
68 cmd.arg(arg);
69 }
70
71 for arg in &tf.global_args {
74 cmd.arg(arg);
75 }
76
77 for (key, value) in &tf.env {
79 cmd.env(key, value);
80 }
81
82 cmd.stdout(Stdio::piped());
83 cmd.stderr(Stdio::piped());
84
85 trace!(binary = ?tf.binary, args = ?command_args, "executing terraform command");
86
87 let output = cmd.output().await.map_err(|e| {
88 if e.kind() == std::io::ErrorKind::NotFound {
89 Error::NotFound
90 } else {
91 Error::Io {
92 message: format!("failed to execute terraform: {e}"),
93 source: e,
94 }
95 }
96 })?;
97
98 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
99 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
100 let exit_code = output.status.code().unwrap_or(-1);
101 let success = allowed_codes.contains(&exit_code);
102
103 debug!(exit_code, success, "terraform command completed");
104 trace!(%stdout, "stdout");
105 if !stderr.is_empty() {
106 trace!(%stderr, "stderr");
107 }
108
109 if !success {
110 return Err(Error::CommandFailed {
111 command: command_args.first().cloned().unwrap_or_default(),
112 exit_code,
113 stdout,
114 stderr,
115 });
116 }
117
118 Ok(CommandOutput {
119 stdout,
120 stderr,
121 exit_code,
122 success,
123 })
124}