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 if let Some(subcommand) = command_args.first() {
68 cmd.arg(subcommand);
69 }
70
71 for arg in &tf.global_args {
73 cmd.arg(arg);
74 }
75
76 for arg in command_args.iter().skip(1) {
78 cmd.arg(arg);
79 }
80
81 for (key, value) in &tf.env {
83 cmd.env(key, value);
84 }
85
86 cmd.stdout(Stdio::piped());
87 cmd.stderr(Stdio::piped());
88
89 trace!(binary = ?tf.binary, args = ?command_args, "executing terraform command");
90
91 let output = cmd.output().await.map_err(|e| {
92 if e.kind() == std::io::ErrorKind::NotFound {
93 Error::NotFound
94 } else {
95 Error::Io {
96 message: format!("failed to execute terraform: {e}"),
97 source: e,
98 }
99 }
100 })?;
101
102 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
103 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
104 let exit_code = output.status.code().unwrap_or(-1);
105 let success = allowed_codes.contains(&exit_code);
106
107 debug!(exit_code, success, "terraform command completed");
108 trace!(%stdout, "stdout");
109 if !stderr.is_empty() {
110 trace!(%stderr, "stderr");
111 }
112
113 if !success {
114 return Err(Error::CommandFailed {
115 command: command_args.first().cloned().unwrap_or_default(),
116 exit_code,
117 stdout,
118 stderr,
119 });
120 }
121
122 Ok(CommandOutput {
123 stdout,
124 stderr,
125 exit_code,
126 success,
127 })
128}