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