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