Skip to main content

alien_bindings/providers/build/
codebuild.rs

1use 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/// 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                    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    /// Convert AWS CodeBuild status string to alien BuildStatus
86    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        // Merge build config environment with binding environment variables
103        // Build config environment takes precedence over binding environment
104        let mut merged_env = self.build_env_vars.clone();
105        merged_env.extend(config.environment);
106
107        // Merge monitoring configuration - build config takes precedence over binding
108        let monitoring = config.monitoring.or_else(|| self.monitoring.clone());
109
110        // Convert environment variables
111        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        // Create buildspec content with the unified wrapper script
122        let wrapper_script = create_build_wrapper_script(&config.script, monitoring.as_ref());
123
124        // Properly indent the wrapper script for YAML literal block
125        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 {}