Skip to main content

alien_bindings/providers/build/
codebuild.rs

1use 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/// AWS implementation of the `Build` trait using CodeBuild.
20#[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    /// Creates a new AWS Build instance from binding parameters.
31    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        // Extract values from binding
42        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    /// Convert AWS CodeBuild status string to alien BuildStatus
90    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        // Merge build config environment with binding environment variables
107        // Build config environment takes precedence over binding environment
108        let mut merged_env = self.build_env_vars.clone();
109        merged_env.extend(config.environment);
110
111        // Merge monitoring configuration - build config takes precedence over binding
112        let monitoring = config.monitoring.or_else(|| self.monitoring.clone());
113
114        // Convert environment variables
115        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        // Create buildspec content with the unified wrapper script
126        let wrapper_script = create_build_wrapper_script(&config.script, monitoring.as_ref());
127
128        // Properly indent the wrapper script for YAML literal block
129        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 {}