1use crate::gcloud::GcloudError;
2
3#[allow(async_fn_in_trait)]
8pub trait GcloudExecutor: Send + Sync {
9 async fn exec(&self, args: &[String]) -> Result<String, GcloudError>;
11
12 async fn exec_streaming(&self, args: &[String]) -> Result<(), GcloudError>;
14
15 async fn exec_with_stdin(
17 &self,
18 args: &[String],
19 stdin_data: &[u8],
20 ) -> Result<String, GcloudError>;
21}
22
23pub struct RealExecutor;
25
26impl GcloudExecutor for RealExecutor {
27 async fn exec(&self, args: &[String]) -> Result<String, GcloudError> {
28 use std::process::Stdio;
29
30 tracing::debug!(cmd = %format!("gcloud {}", args.join(" ")), "exec");
31
32 let output = tokio::process::Command::new("gcloud")
33 .args(args)
34 .stdout(Stdio::piped())
35 .stderr(Stdio::piped())
36 .output()
37 .await
38 .map_err(|e| GcloudError::NotFound { source: e })?;
39
40 if output.status.success() {
41 String::from_utf8(output.stdout).map_err(|e| GcloudError::InvalidUtf8 { source: e })
42 } else {
43 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
44 tracing::warn!(cmd = %format!("gcloud {}", args.join(" ")), %stderr, "command failed");
45 Err(GcloudError::CommandFailed {
46 args: args.to_vec(),
47 stderr,
48 })
49 }
50 }
51
52 async fn exec_streaming(&self, args: &[String]) -> Result<(), GcloudError> {
53 use std::process::Stdio;
54
55 tracing::debug!(cmd = %format!("gcloud {}", args.join(" ")), "exec_streaming");
56
57 let status = tokio::process::Command::new("gcloud")
58 .args(args)
59 .stdout(Stdio::inherit())
60 .stderr(Stdio::inherit())
61 .status()
62 .await
63 .map_err(|e| GcloudError::NotFound { source: e })?;
64
65 if status.success() {
66 Ok(())
67 } else {
68 tracing::warn!(cmd = %format!("gcloud {}", args.join(" ")), %status, "streaming command failed");
69 Err(GcloudError::CommandFailed {
70 args: args.to_vec(),
71 stderr: format!("exit code: {status}"),
72 })
73 }
74 }
75
76 async fn exec_with_stdin(
77 &self,
78 args: &[String],
79 stdin_data: &[u8],
80 ) -> Result<String, GcloudError> {
81 use std::process::Stdio;
82 use tokio::io::AsyncWriteExt;
83
84 tracing::debug!(
85 cmd = %format!("gcloud {}", args.join(" ")),
86 stdin_bytes = stdin_data.len(),
87 "exec_with_stdin"
88 );
89
90 let mut child = tokio::process::Command::new("gcloud")
91 .args(args)
92 .stdin(Stdio::piped())
93 .stdout(Stdio::piped())
94 .stderr(Stdio::piped())
95 .spawn()
96 .map_err(|e| GcloudError::NotFound { source: e })?;
97
98 if let Some(mut stdin) = child.stdin.take() {
99 stdin
100 .write_all(stdin_data)
101 .await
102 .map_err(|e| GcloudError::StdinWrite { source: e })?;
103 stdin
104 .shutdown()
105 .await
106 .map_err(|e| GcloudError::StdinWrite { source: e })?;
107 }
108
109 let output = child
110 .wait_with_output()
111 .await
112 .map_err(|e| GcloudError::NotFound { source: e })?;
113
114 if output.status.success() {
115 String::from_utf8(output.stdout).map_err(|e| GcloudError::InvalidUtf8 { source: e })
116 } else {
117 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
118 tracing::warn!(cmd = %format!("gcloud {}", args.join(" ")), %stderr, "command failed");
119 Err(GcloudError::CommandFailed {
120 args: args.to_vec(),
121 stderr,
122 })
123 }
124 }
125}