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