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::{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 binding_name: binding_name.clone(),
47 reason: "Expected CodeBuild binding, got different service type".to_string(),
48 }));
49 }
50 };
51
52 let project_name = config
53 .project_name
54 .into_value(&binding_name, "project_name")
55 .context(ErrorData::BindingConfigInvalid {
56 binding_name: binding_name.clone(),
57 reason: "Failed to extract project_name from binding".to_string(),
58 })?;
59
60 let build_env_vars = config
61 .build_env_vars
62 .into_value(&binding_name, "build_env_vars")
63 .context(ErrorData::BindingConfigInvalid {
64 binding_name: binding_name.clone(),
65 reason: "Failed to extract build_env_vars from binding".to_string(),
66 })?;
67
68 let monitoring = config
69 .monitoring
70 .into_value(&binding_name, "monitoring")
71 .context(ErrorData::BindingConfigInvalid {
72 binding_name: binding_name.clone(),
73 reason: "Failed to extract monitoring from binding".to_string(),
74 })?;
75
76 Ok(Self {
77 client,
78 binding_name,
79 project_name,
80 build_env_vars,
81 monitoring,
82 })
83 }
84
85 fn map_build_status(status: Option<&str>) -> BuildStatus {
87 match status {
88 Some("SUCCEEDED") => BuildStatus::Succeeded,
89 Some("FAILED") | Some("FAULT") => BuildStatus::Failed,
90 Some("STOPPED") => BuildStatus::Cancelled,
91 Some("TIMED_OUT") => BuildStatus::TimedOut,
92 Some("IN_PROGRESS") => BuildStatus::Running,
93 Some("NOT_STARTED") => BuildStatus::Queued,
94 _ => BuildStatus::Queued,
95 }
96 }
97}
98
99#[async_trait]
100impl Build for CodebuildBuild {
101 async fn start_build(&self, config: BuildConfig) -> Result<BuildExecution, Error> {
102 let mut merged_env = self.build_env_vars.clone();
105 merged_env.extend(config.environment);
106
107 let monitoring = config.monitoring.or_else(|| self.monitoring.clone());
109
110 let env_vars: Vec<EnvironmentVariable> = merged_env
112 .iter()
113 .map(|(key, value)| {
114 EnvironmentVariable::builder()
115 .name(key.clone())
116 .value(value.clone())
117 .build()
118 })
119 .collect();
120
121 let wrapper_script = create_build_wrapper_script(&config.script, monitoring.as_ref());
123
124 let indented_wrapper_script = wrapper_script
126 .lines()
127 .map(|line| {
128 if line.trim().is_empty() {
129 String::new()
130 } else {
131 format!(" {}", line)
132 }
133 })
134 .collect::<Vec<_>>()
135 .join("\n");
136
137 let buildspec_content = format!(
138 r#"version: 0.2
139phases:
140 build:
141 commands:
142 - |
143{}
144"#,
145 indented_wrapper_script
146 );
147
148 let start_build_request = StartBuildRequest::builder()
149 .project_name(self.project_name.clone())
150 .buildspec_override(buildspec_content)
151 .environment_variables_override(env_vars)
152 .build();
153
154 let start_build_response =
155 self.client
156 .start_build(start_build_request)
157 .await
158 .map_err(|e| {
159 map_cloud_client_error(
160 e,
161 format!("Failed to start CodeBuild build '{}'", self.project_name),
162 None,
163 )
164 })?;
165
166 let build = &start_build_response.build;
167 let build_id = build.id.as_deref().unwrap_or_default();
168 let status = Self::map_build_status(build.build_status.as_deref());
169 let start_time = build.start_time.map(|t| {
170 chrono::DateTime::from_timestamp(t as i64, 0)
171 .unwrap_or_default()
172 .to_rfc3339()
173 });
174
175 Ok(BuildExecution {
176 id: build_id.to_string(),
177 status,
178 start_time,
179 end_time: None,
180 })
181 }
182
183 async fn get_build_status(&self, build_id: &str) -> Result<BuildExecution, Error> {
184 let batch_get_builds_request = BatchGetBuildsRequest::builder()
185 .ids(vec![build_id.to_string()])
186 .build();
187
188 let batch_get_builds_response = self
189 .client
190 .batch_get_builds(batch_get_builds_request)
191 .await
192 .map_err(|e| {
193 map_cloud_client_error(
194 e,
195 format!("Failed to get CodeBuild status for build '{}'", build_id),
196 Some(build_id.to_string()),
197 )
198 })?;
199
200 let build = batch_get_builds_response
201 .builds
202 .as_ref()
203 .and_then(|builds| builds.first())
204 .ok_or_else(|| {
205 AlienError::new(ErrorData::BuildOperationFailed {
206 binding_name: self.binding_name.clone(),
207 operation: format!("find build {}", build_id),
208 })
209 })?;
210
211 let status = Self::map_build_status(build.build_status.as_deref());
212 let start_time = build.start_time.map(|t| {
213 chrono::DateTime::from_timestamp(t as i64, 0)
214 .unwrap_or_default()
215 .to_rfc3339()
216 });
217 let end_time = build.end_time.map(|t| {
218 chrono::DateTime::from_timestamp(t as i64, 0)
219 .unwrap_or_default()
220 .to_rfc3339()
221 });
222
223 Ok(BuildExecution {
224 id: build_id.to_string(),
225 status,
226 start_time,
227 end_time,
228 })
229 }
230
231 async fn stop_build(&self, build_id: &str) -> Result<(), Error> {
232 let stop_build_request = StopBuildRequest::builder().id(build_id.to_string()).build();
233
234 self.client
235 .stop_build(stop_build_request)
236 .await
237 .map_err(|e| {
238 map_cloud_client_error(
239 e,
240 format!("Failed to stop CodeBuild build '{}'", build_id),
241 Some(build_id.to_string()),
242 )
243 })?;
244
245 Ok(())
246 }
247}
248
249impl Binding for CodebuildBuild {}