alien_bindings/providers/build/
codebuild.rs1use crate::{
2 error::{binding_env_var, map_cloud_client_error, Error, ErrorData},
3 providers::build::script::create_build_wrapper_script,
4 traits::{Binding, Build},
5};
6use alien_core::{bindings::BuildBinding, BuildConfig, BuildExecution, BuildStatus};
7use alien_error::{AlienError, Context};
8use async_trait::async_trait;
9use std::collections::HashMap;
10
11use alien_aws_clients::{
12 codebuild::{
13 BatchGetBuildsRequest, CodeBuildApi, CodeBuildClient, EnvironmentVariable,
14 StartBuildRequest, StopBuildRequest,
15 },
16 AwsCredentialProvider,
17};
18
19#[derive(Debug)]
21pub struct CodebuildBuild {
22 client: CodeBuildClient,
23 binding_name: String,
24 project_name: String,
25 build_env_vars: HashMap<String, String>,
26 monitoring: Option<alien_core::MonitoringConfig>,
27}
28
29impl CodebuildBuild {
30 pub async fn new(
32 binding_name: String,
33 binding: BuildBinding,
34 credentials: &AwsCredentialProvider,
35 ) -> Result<Self, Error> {
36 let client = CodeBuildClient::new(
37 crate::http_client::create_http_client(),
38 credentials.clone(),
39 );
40
41 let config = match binding {
43 BuildBinding::Codebuild(config) => config,
44 _ => {
45 return Err(Error::new(ErrorData::BindingConfigInvalid {
46 env_var: binding_env_var(&binding_name),
47 binding_name: binding_name.clone(),
48 reason: "Expected CodeBuild binding, got different service type".to_string(),
49 }));
50 }
51 };
52
53 let project_name = config
54 .project_name
55 .into_value(&binding_name, "project_name")
56 .context(ErrorData::BindingConfigInvalid {
57 env_var: binding_env_var(&binding_name),
58 binding_name: binding_name.clone(),
59 reason: "Failed to extract project_name from binding".to_string(),
60 })?;
61
62 let build_env_vars = config
63 .build_env_vars
64 .into_value(&binding_name, "build_env_vars")
65 .context(ErrorData::BindingConfigInvalid {
66 env_var: binding_env_var(&binding_name),
67 binding_name: binding_name.clone(),
68 reason: "Failed to extract build_env_vars from binding".to_string(),
69 })?;
70
71 let monitoring = config
72 .monitoring
73 .into_value(&binding_name, "monitoring")
74 .context(ErrorData::BindingConfigInvalid {
75 env_var: binding_env_var(&binding_name),
76 binding_name: binding_name.clone(),
77 reason: "Failed to extract monitoring from binding".to_string(),
78 })?;
79
80 Ok(Self {
81 client,
82 binding_name,
83 project_name,
84 build_env_vars,
85 monitoring,
86 })
87 }
88
89 fn map_build_status(status: Option<&str>) -> BuildStatus {
91 match status {
92 Some("SUCCEEDED") => BuildStatus::Succeeded,
93 Some("FAILED") | Some("FAULT") => BuildStatus::Failed,
94 Some("STOPPED") => BuildStatus::Cancelled,
95 Some("TIMED_OUT") => BuildStatus::TimedOut,
96 Some("IN_PROGRESS") => BuildStatus::Running,
97 Some("NOT_STARTED") => BuildStatus::Queued,
98 _ => BuildStatus::Queued,
99 }
100 }
101}
102
103#[async_trait]
104impl Build for CodebuildBuild {
105 async fn start_build(&self, config: BuildConfig) -> Result<BuildExecution, Error> {
106 let mut merged_env = self.build_env_vars.clone();
109 merged_env.extend(config.environment);
110
111 let monitoring = config.monitoring.or_else(|| self.monitoring.clone());
113
114 let env_vars: Vec<EnvironmentVariable> = merged_env
116 .iter()
117 .map(|(key, value)| {
118 EnvironmentVariable::builder()
119 .name(key.clone())
120 .value(value.clone())
121 .build()
122 })
123 .collect();
124
125 let wrapper_script = create_build_wrapper_script(&config.script, monitoring.as_ref());
127
128 let indented_wrapper_script = wrapper_script
130 .lines()
131 .map(|line| {
132 if line.trim().is_empty() {
133 String::new()
134 } else {
135 format!(" {}", line)
136 }
137 })
138 .collect::<Vec<_>>()
139 .join("\n");
140
141 let buildspec_content = format!(
142 r#"version: 0.2
143phases:
144 build:
145 commands:
146 - |
147{}
148"#,
149 indented_wrapper_script
150 );
151
152 let start_build_request = StartBuildRequest::builder()
153 .project_name(self.project_name.clone())
154 .buildspec_override(buildspec_content)
155 .environment_variables_override(env_vars)
156 .build();
157
158 let start_build_response =
159 self.client
160 .start_build(start_build_request)
161 .await
162 .map_err(|e| {
163 map_cloud_client_error(
164 e,
165 format!("Failed to start CodeBuild build '{}'", self.project_name),
166 None,
167 )
168 })?;
169
170 let build = &start_build_response.build;
171 let build_id = build.id.as_deref().unwrap_or_default();
172 let status = Self::map_build_status(build.build_status.as_deref());
173 let start_time = build.start_time.map(|t| {
174 chrono::DateTime::from_timestamp(t as i64, 0)
175 .unwrap_or_default()
176 .to_rfc3339()
177 });
178
179 Ok(BuildExecution {
180 id: build_id.to_string(),
181 status,
182 start_time,
183 end_time: None,
184 })
185 }
186
187 async fn get_build_status(&self, build_id: &str) -> Result<BuildExecution, Error> {
188 let batch_get_builds_request = BatchGetBuildsRequest::builder()
189 .ids(vec![build_id.to_string()])
190 .build();
191
192 let batch_get_builds_response = self
193 .client
194 .batch_get_builds(batch_get_builds_request)
195 .await
196 .map_err(|e| {
197 map_cloud_client_error(
198 e,
199 format!("Failed to get CodeBuild status for build '{}'", build_id),
200 Some(build_id.to_string()),
201 )
202 })?;
203
204 let build = batch_get_builds_response
205 .builds
206 .as_ref()
207 .and_then(|builds| builds.first())
208 .ok_or_else(|| {
209 AlienError::new(ErrorData::BuildOperationFailed {
210 binding_name: self.binding_name.clone(),
211 operation: format!("find build {}", build_id),
212 })
213 })?;
214
215 let status = Self::map_build_status(build.build_status.as_deref());
216 let start_time = build.start_time.map(|t| {
217 chrono::DateTime::from_timestamp(t as i64, 0)
218 .unwrap_or_default()
219 .to_rfc3339()
220 });
221 let end_time = build.end_time.map(|t| {
222 chrono::DateTime::from_timestamp(t as i64, 0)
223 .unwrap_or_default()
224 .to_rfc3339()
225 });
226
227 Ok(BuildExecution {
228 id: build_id.to_string(),
229 status,
230 start_time,
231 end_time,
232 })
233 }
234
235 async fn stop_build(&self, build_id: &str) -> Result<(), Error> {
236 let stop_build_request = StopBuildRequest::builder().id(build_id.to_string()).build();
237
238 self.client
239 .stop_build(stop_build_request)
240 .await
241 .map_err(|e| {
242 map_cloud_client_error(
243 e,
244 format!("Failed to stop CodeBuild build '{}'", build_id),
245 Some(build_id.to_string()),
246 )
247 })?;
248
249 Ok(())
250 }
251}
252
253impl Binding for CodebuildBuild {}