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::{
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/// AWS implementation of the `Build` trait using CodeBuild.
23#[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    /// Creates a new AWS Build instance from binding parameters.
34    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        // Extract values from binding
43        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    /// Convert AWS CodeBuild status string to alien BuildStatus
87    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        // Merge build config environment with binding environment variables
104        // Build config environment takes precedence over binding environment
105        let mut merged_env = self.build_env_vars.clone();
106        merged_env.extend(config.environment);
107
108        // Merge monitoring configuration - build config takes precedence over binding
109        let monitoring = config.monitoring.or_else(|| self.monitoring.clone());
110
111        // Convert environment variables
112        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        // Create buildspec content with the unified wrapper script
123        let wrapper_script = create_build_wrapper_script(&config.script, monitoring.as_ref());
124
125        // Properly indent the wrapper script for YAML literal block
126        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 {}