alien_bindings/providers/build/
local.rs1use crate::{
2 error::{ErrorData, Result},
3 traits::{Binding, Build},
4};
5use alien_core::{bindings::BuildBinding, BuildConfig, BuildExecution, BuildStatus};
6use alien_error::{AlienError, Context, IntoAlienError};
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use std::{
10 process::Stdio,
11 time::{SystemTime, UNIX_EPOCH},
12};
13use tokio::process::Command;
14use uuid::Uuid;
15
16#[derive(Debug, Serialize, Deserialize)]
17struct BuildMetadata {
18 uuid: String,
19 pid: u32,
20 start_time: String,
21 end_time: Option<String>,
22 status: BuildStatus,
23}
24
25#[derive(Debug)]
29pub struct LocalBuild {
30 binding_name: String,
31 base_dir: std::path::PathBuf,
32 build_env_vars: std::collections::HashMap<String, String>,
33}
34
35impl LocalBuild {
36 pub fn new(binding_name: String, binding: alien_core::bindings::BuildBinding) -> Result<Self> {
38 let config = match binding {
40 BuildBinding::Local(config) => config,
41 _ => {
42 return Err(AlienError::new(ErrorData::BindingConfigInvalid {
43 binding_name: binding_name.clone(),
44 reason: "Expected Local binding, got different service type".to_string(),
45 }));
46 }
47 };
48
49 let data_dir = config
50 .data_dir
51 .into_value(&binding_name, "data_dir")
52 .context(ErrorData::BindingConfigInvalid {
53 binding_name: binding_name.clone(),
54 reason: "Failed to extract data_dir from binding".to_string(),
55 })?;
56
57 let build_env_vars = config
58 .build_env_vars
59 .into_value(&binding_name, "build_env_vars")
60 .context(ErrorData::BindingConfigInvalid {
61 binding_name: binding_name.clone(),
62 reason: "Failed to extract build_env_vars from binding".to_string(),
63 })?;
64
65 let base_dir = std::path::PathBuf::from(data_dir).join(&binding_name);
66
67 std::fs::create_dir_all(&base_dir)
69 .into_alien_error()
70 .context(ErrorData::BindingConfigInvalid {
71 binding_name: binding_name.clone(),
72 reason: "Failed to create build directory".to_string(),
73 })?;
74
75 Ok(Self {
76 binding_name,
77 base_dir,
78 build_env_vars,
79 })
80 }
81
82 pub fn new_from_path(binding_name: String, base_dir: std::path::PathBuf) -> Self {
84 Self {
85 binding_name,
86 base_dir,
87 build_env_vars: std::collections::HashMap::new(),
88 }
89 }
90
91 fn encode_build_id(uuid: &str, pid: u32, timestamp: u64) -> String {
93 format!("{}_{}_{}", uuid, pid, timestamp)
94 }
95
96 fn decode_build_id(build_id: &str) -> Result<(String, u32, u64)> {
98 let parts: Vec<&str> = build_id.split('_').collect();
99 if parts.len() != 3 {
100 return Err(AlienError::new(ErrorData::BuildOperationFailed {
101 binding_name: "local".to_string(),
102 operation: format!("invalid build ID format: {}", build_id),
103 }));
104 }
105
106 let uuid = parts[0].to_string();
107 let pid = parts[1].parse::<u32>().map_err(|_| {
108 AlienError::new(ErrorData::BuildOperationFailed {
109 binding_name: "local".to_string(),
110 operation: format!("invalid PID in build ID: {}", build_id),
111 })
112 })?;
113 let timestamp = parts[2].parse::<u64>().map_err(|_| {
114 AlienError::new(ErrorData::BuildOperationFailed {
115 binding_name: "local".to_string(),
116 operation: format!("invalid timestamp in build ID: {}", build_id),
117 })
118 })?;
119
120 Ok((uuid, pid, timestamp))
121 }
122
123 fn create_build_dir(&self, uuid: &str) -> Result<std::path::PathBuf> {
125 let build_dir = self.base_dir.join("builds").join(uuid);
126 std::fs::create_dir_all(&build_dir)
127 .into_alien_error()
128 .context(ErrorData::BuildOperationFailed {
129 binding_name: self.binding_name.clone(),
130 operation: "create build directory".to_string(),
131 })?;
132 Ok(build_dir)
133 }
134
135 fn save_build_metadata(&self, uuid: &str, metadata: &BuildMetadata) -> Result<()> {
137 let build_dir = self.base_dir.join("builds").join(uuid);
138 let metadata_path = build_dir.join("metadata.json");
139
140 let metadata_json = serde_json::to_string_pretty(metadata)
141 .into_alien_error()
142 .context(ErrorData::BuildOperationFailed {
143 binding_name: self.binding_name.clone(),
144 operation: "serialize build metadata".to_string(),
145 })?;
146
147 std::fs::write(&metadata_path, metadata_json)
148 .into_alien_error()
149 .context(ErrorData::BuildOperationFailed {
150 binding_name: self.binding_name.clone(),
151 operation: "write build metadata".to_string(),
152 })?;
153
154 Ok(())
155 }
156
157 fn load_build_metadata(&self, uuid: &str) -> Result<BuildMetadata> {
159 let build_dir = self.base_dir.join("builds").join(uuid);
160 let metadata_path = build_dir.join("metadata.json");
161
162 let metadata_json = std::fs::read_to_string(&metadata_path)
163 .into_alien_error()
164 .context(ErrorData::BuildOperationFailed {
165 binding_name: self.binding_name.clone(),
166 operation: format!("read build metadata for {}", uuid),
167 })?;
168
169 let metadata: BuildMetadata = serde_json::from_str(&metadata_json)
170 .into_alien_error()
171 .context(ErrorData::BuildOperationFailed {
172 binding_name: self.binding_name.clone(),
173 operation: format!("parse build metadata for {}", uuid),
174 })?;
175
176 Ok(metadata)
177 }
178
179 fn is_process_running(&self, pid: u32) -> bool {
181 #[cfg(unix)]
182 {
183 use std::process::Command;
184 Command::new("kill")
186 .args(["-0", &pid.to_string()])
187 .output()
188 .map(|output| output.status.success())
189 .unwrap_or(false)
190 }
191
192 #[cfg(windows)]
193 {
194 use std::process::Command;
195 Command::new("tasklist")
197 .args(["/FI", &format!("PID eq {}", pid)])
198 .output()
199 .map(|output| {
200 output.status.success()
201 && String::from_utf8_lossy(&output.stdout).contains(&pid.to_string())
202 })
203 .unwrap_or(false)
204 }
205 }
206
207 fn update_build_status(&self, metadata: &mut BuildMetadata) -> Result<()> {
209 if metadata.status == BuildStatus::Running {
210 if !self.is_process_running(metadata.pid) {
211 metadata.status = BuildStatus::Succeeded; metadata.end_time = Some(chrono::Utc::now().to_rfc3339());
214 self.save_build_metadata(&metadata.uuid, metadata)?;
215 }
216 }
217 Ok(())
218 }
219}
220
221#[async_trait]
222impl Build for LocalBuild {
223 async fn start_build(&self, config: BuildConfig) -> Result<BuildExecution> {
224 let uuid = Uuid::new_v4().to_string();
225 let start_time = chrono::Utc::now().to_rfc3339();
226 let timestamp = SystemTime::now()
227 .duration_since(UNIX_EPOCH)
228 .unwrap()
229 .as_secs();
230
231 let build_dir = self.create_build_dir(&uuid)?;
233
234 let script_path = build_dir.join("build_script.sh");
236 std::fs::write(&script_path, &config.script)
237 .into_alien_error()
238 .context(ErrorData::BuildOperationFailed {
239 binding_name: self.binding_name.clone(),
240 operation: "write build script".to_string(),
241 })?;
242
243 #[cfg(unix)]
245 {
246 use std::os::unix::fs::PermissionsExt;
247 let mut perms = std::fs::metadata(&script_path)
248 .into_alien_error()
249 .context(ErrorData::BuildOperationFailed {
250 binding_name: self.binding_name.clone(),
251 operation: "get script permissions".to_string(),
252 })?
253 .permissions();
254 perms.set_mode(0o755);
255 std::fs::set_permissions(&script_path, perms)
256 .into_alien_error()
257 .context(ErrorData::BuildOperationFailed {
258 binding_name: self.binding_name.clone(),
259 operation: "set script permissions".to_string(),
260 })?;
261 }
262
263 let mut cmd = Command::new("bash");
265 cmd.arg(&script_path)
266 .current_dir(&build_dir)
267 .stdin(Stdio::null())
268 .stdout(Stdio::piped())
269 .stderr(Stdio::piped());
270
271 let mut merged_environment = self.build_env_vars.clone();
274 merged_environment.extend(config.environment);
275
276 for (key, value) in &merged_environment {
278 cmd.env(key, value);
279 }
280
281 let mut child =
283 cmd.spawn()
284 .into_alien_error()
285 .context(ErrorData::BuildOperationFailed {
286 binding_name: self.binding_name.clone(),
287 operation: "start build process".to_string(),
288 })?;
289
290 let pid = child.id().ok_or_else(|| {
292 AlienError::new(ErrorData::BuildOperationFailed {
293 binding_name: self.binding_name.clone(),
294 operation: "get process ID".to_string(),
295 })
296 })?;
297
298 let build_id = Self::encode_build_id(&uuid, pid, timestamp);
300
301 let metadata = BuildMetadata {
303 uuid: uuid.clone(),
304 pid,
305 start_time: start_time.clone(),
306 end_time: None,
307 status: BuildStatus::Running,
308 };
309 self.save_build_metadata(&uuid, &metadata)?;
310
311 tokio::spawn(async move {
313 let _ = child.wait().await;
314 });
315
316 Ok(BuildExecution {
317 id: build_id,
318 status: BuildStatus::Running,
319 start_time: Some(start_time),
320 end_time: None,
321 })
322 }
323
324 async fn get_build_status(&self, build_id: &str) -> Result<BuildExecution> {
325 let (uuid, _pid, _timestamp) = Self::decode_build_id(build_id)?;
327
328 let mut metadata = self.load_build_metadata(&uuid)?;
330
331 self.update_build_status(&mut metadata)?;
333
334 Ok(BuildExecution {
335 id: build_id.to_string(),
336 status: metadata.status,
337 start_time: Some(metadata.start_time),
338 end_time: metadata.end_time,
339 })
340 }
341
342 async fn stop_build(&self, build_id: &str) -> Result<()> {
343 let (uuid, pid, _timestamp) = Self::decode_build_id(build_id)?;
345
346 let mut metadata = self.load_build_metadata(&uuid)?;
348
349 if metadata.status == BuildStatus::Running {
351 #[cfg(unix)]
352 {
353 use std::process::Command;
354 let _ = Command::new("kill")
356 .args(["-TERM", &pid.to_string()])
357 .output();
358 }
359
360 #[cfg(windows)]
361 {
362 use std::process::Command;
363 let _ = Command::new("taskkill")
365 .args(["/PID", &pid.to_string(), "/F"])
366 .output();
367 }
368
369 metadata.status = BuildStatus::Cancelled;
371 metadata.end_time = Some(chrono::Utc::now().to_rfc3339());
372 self.save_build_metadata(&uuid, &metadata)?;
373 }
374
375 Ok(())
376 }
377}
378
379impl Binding for LocalBuild {}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use std::collections::HashMap;
385 use tempfile::TempDir;
386
387 #[tokio::test]
388 async fn test_local_build_success() {
389 let temp_dir = TempDir::new().unwrap();
390 let local_build =
391 LocalBuild::new_from_path("test-build".to_string(), temp_dir.path().to_path_buf());
392
393 let mut config = BuildConfig {
394 image: "ubuntu:20.04".to_string(), script: "echo 'Hello World!'".to_string(),
396 environment: HashMap::new(),
397 timeout_seconds: 30,
398 compute_type: alien_core::ComputeType::Small,
399 monitoring: None,
400 };
401 config
402 .environment
403 .insert("TEST_VAR".to_string(), "test_value".to_string());
404
405 let execution = local_build.start_build(config).await.unwrap();
406 assert!(!execution.id.is_empty());
407 assert_eq!(execution.status, BuildStatus::Running);
408
409 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
411
412 let status = local_build.get_build_status(&execution.id).await.unwrap();
413 assert_eq!(status.status, BuildStatus::Succeeded);
414 assert!(status.end_time.is_some());
415 }
416
417 #[tokio::test]
418 async fn test_local_build_failure() {
419 let temp_dir = TempDir::new().unwrap();
420 let local_build =
421 LocalBuild::new_from_path("test-build".to_string(), temp_dir.path().to_path_buf());
422
423 let config = BuildConfig {
424 image: "ubuntu:20.04".to_string(), script: "exit 1".to_string(), environment: HashMap::new(),
427 timeout_seconds: 30,
428 compute_type: alien_core::ComputeType::Small,
429 monitoring: None,
430 };
431
432 let execution = local_build.start_build(config).await.unwrap();
433 assert!(!execution.id.is_empty());
434 assert_eq!(execution.status, BuildStatus::Running);
435
436 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
438
439 let status = local_build.get_build_status(&execution.id).await.unwrap();
440 assert_eq!(status.status, BuildStatus::Succeeded); assert!(status.end_time.is_some());
442 }
443
444 #[tokio::test]
445 async fn test_local_build_stop() {
446 let temp_dir = TempDir::new().unwrap();
447 let local_build =
448 LocalBuild::new_from_path("test-build".to_string(), temp_dir.path().to_path_buf());
449
450 let config = BuildConfig {
451 image: "ubuntu:20.04".to_string(), script: "sleep 10".to_string(), environment: HashMap::new(),
454 timeout_seconds: 30,
455 compute_type: alien_core::ComputeType::Small,
456 monitoring: None,
457 };
458
459 let execution = local_build.start_build(config).await.unwrap();
460 assert!(!execution.id.is_empty());
461 assert_eq!(execution.status, BuildStatus::Running);
462
463 local_build.stop_build(&execution.id).await.unwrap();
465
466 let status = local_build.get_build_status(&execution.id).await.unwrap();
467 assert_eq!(status.status, BuildStatus::Cancelled);
468 assert!(status.end_time.is_some());
469 }
470
471 #[test]
472 fn test_build_id_encoding_decoding() {
473 let uuid = "550e8400-e29b-41d4-a716-446655440000";
474 let pid = 12345u32;
475 let timestamp = 1234567890u64;
476
477 let build_id = LocalBuild::encode_build_id(uuid, pid, timestamp);
478 let (decoded_uuid, decoded_pid, decoded_timestamp) =
479 LocalBuild::decode_build_id(&build_id).unwrap();
480
481 assert_eq!(decoded_uuid, uuid);
482 assert_eq!(decoded_pid, pid);
483 assert_eq!(decoded_timestamp, timestamp);
484 }
485}