1use crate::error::{ErrorData, Result};
2use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef, ResourceType};
3use alien_error::AlienError;
4use bon::Builder;
5use serde::{Deserialize, Serialize};
6use std::any::Any;
7use std::collections::HashMap;
8use std::fmt::Debug;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
14pub enum BuildStatus {
15 Queued,
17 Running,
19 Succeeded,
21 Failed,
23 Cancelled,
25 TimedOut,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
32#[serde(rename_all = "kebab-case")]
33pub enum ComputeType {
34 Small,
36 Medium,
38 Large,
40 XLarge,
42}
43
44impl Default for ComputeType {
45 fn default() -> Self {
46 ComputeType::Medium
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
53#[serde(rename_all = "camelCase")]
54pub struct MonitoringConfig {
55 pub endpoint: String,
57 #[serde(default)]
59 pub headers: HashMap<String, String>,
60 #[serde(default = "default_logs_uri")]
62 pub logs_uri: String,
63 #[serde(default = "default_tls_enabled")]
65 pub tls_enabled: bool,
66 #[serde(default = "default_tls_verify")]
68 pub tls_verify: bool,
69}
70
71fn default_logs_uri() -> String {
72 "/v1/logs".to_string()
73}
74
75fn default_tls_enabled() -> bool {
76 true
77}
78
79fn default_tls_verify() -> bool {
80 true
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
86#[serde(rename_all = "camelCase")]
87pub struct BuildConfig {
88 pub image: String,
90 pub script: String,
92 #[serde(default)]
94 pub environment: HashMap<String, String>,
95 pub timeout_seconds: u32,
97 #[serde(default)]
99 pub compute_type: ComputeType,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub monitoring: Option<MonitoringConfig>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
109#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
110#[serde(rename_all = "camelCase", deny_unknown_fields)]
111#[builder(start_fn = new)]
112pub struct Build {
113 #[builder(start_fn)]
116 pub id: String,
117
118 #[builder(field)]
120 pub links: Vec<ResourceRef>,
121
122 pub permissions: String,
125
126 #[builder(default)]
128 #[serde(default)]
129 pub environment: HashMap<String, String>,
130
131 #[builder(default)]
133 #[serde(default)]
134 pub compute_type: ComputeType,
135}
136
137impl Build {
138 pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("build");
140
141 pub fn get_permissions(&self) -> &str {
143 &self.permissions
144 }
145}
146
147use crate::resources::build::build_builder::State;
148
149impl<S: State> BuildBuilder<S> {
150 pub fn link<R: ?Sized>(mut self, resource: &R) -> Self
153 where
154 for<'a> &'a R: Into<ResourceRef>, {
156 let resource_ref: ResourceRef = resource.into();
158 self.links.push(resource_ref);
159 self
160 }
161}
162
163impl ResourceDefinition for Build {
165 fn get_resource_type(&self) -> ResourceType {
166 Self::RESOURCE_TYPE
167 }
168
169 fn id(&self) -> &str {
170 &self.id
171 }
172
173 fn get_dependencies(&self) -> Vec<ResourceRef> {
174 self.links.clone()
177 }
178
179 fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
180 let new_build = new_config.as_any().downcast_ref::<Build>().ok_or_else(|| {
182 AlienError::new(ErrorData::UnexpectedResourceType {
183 resource_id: self.id.clone(),
184 expected: Self::RESOURCE_TYPE,
185 actual: new_config.get_resource_type(),
186 })
187 })?;
188
189 if self.id != new_build.id {
190 return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
191 resource_id: self.id.clone(),
192 reason: "the 'id' field is immutable".to_string(),
193 }));
194 }
195 Ok(())
196 }
197
198 fn as_any(&self) -> &dyn Any {
199 self
200 }
201
202 fn as_any_mut(&mut self) -> &mut dyn Any {
203 self
204 }
205
206 fn box_clone(&self) -> Box<dyn ResourceDefinition> {
207 Box::new(self.clone())
208 }
209
210 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
211 other.as_any().downcast_ref::<Build>() == Some(self)
212 }
213
214 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
215 serde_json::to_value(self)
216 }
217}
218
219#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
221#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
222#[serde(rename_all = "camelCase")]
223pub struct BuildOutputs {
224 pub identifier: String,
226}
227
228impl ResourceOutputsDefinition for BuildOutputs {
229 fn get_resource_type(&self) -> ResourceType {
230 Build::RESOURCE_TYPE.clone()
231 }
232
233 fn as_any(&self) -> &dyn Any {
234 self
235 }
236
237 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
238 Box::new(self.clone())
239 }
240
241 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
242 other.as_any().downcast_ref::<BuildOutputs>() == Some(self)
243 }
244
245 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
246 serde_json::to_value(self)
247 }
248}
249
250#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
252#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
253#[serde(rename_all = "camelCase")]
254pub struct BuildExecution {
255 pub id: String,
257 pub status: BuildStatus,
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub start_time: Option<String>,
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub end_time: Option<String>,
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::Storage;
271
272 #[test]
273 fn test_build_builder_direct_refs() {
274 let dummy_storage = Storage::new("test-storage".to_string()).build();
275
276 let build = Build::new("my-build".to_string())
277 .permissions("build-execution".to_string())
278 .link(&dummy_storage) .compute_type(ComputeType::Large)
280 .build();
281
282 assert_eq!(build.id, "my-build");
283 assert_eq!(build.compute_type, ComputeType::Large);
284
285 assert_eq!(build.permissions, "build-execution");
287
288 assert!(build
290 .links
291 .contains(&ResourceRef::new(Storage::RESOURCE_TYPE, "test-storage")));
292 assert_eq!(build.links.len(), 1);
293 }
294
295 #[test]
296 fn test_build_defaults() {
297 let build = Build::new("default-build".to_string())
298 .permissions("build-execution".to_string())
299 .build();
300
301 assert_eq!(build.compute_type, ComputeType::Medium);
302 assert!(build.environment.is_empty());
303 assert!(build.links.is_empty());
304 }
305
306 #[test]
307 fn test_build_with_environment() {
308 let mut env = HashMap::new();
309 env.insert("NODE_ENV".to_string(), "production".to_string());
310 env.insert("API_KEY".to_string(), "secret".to_string());
311
312 let build = Build::new("env-build".to_string())
313 .environment(env.clone())
314 .permissions("test".to_string())
315 .build();
316
317 assert_eq!(build.environment, env);
318 }
319
320 #[test]
321 fn test_build_config_with_monitoring() {
322 let mut headers = HashMap::new();
323 headers.insert("Authorization".to_string(), "Bearer token123".to_string());
324 headers.insert("X-Custom-Header".to_string(), "custom-value".to_string());
325
326 let monitoring_config = MonitoringConfig {
327 endpoint: "https://otel-collector.example.com:4318".to_string(),
328 headers,
329 logs_uri: "/v1/logs".to_string(),
330 tls_enabled: true,
331 tls_verify: false,
332 };
333
334 let build_config = BuildConfig {
335 image: "ubuntu:20.04".to_string(),
336 script: "echo 'Hello World'".to_string(),
337 environment: HashMap::new(),
338 timeout_seconds: 300,
339 compute_type: ComputeType::Medium,
340 monitoring: Some(monitoring_config.clone()),
341 };
342
343 assert_eq!(build_config.image, "ubuntu:20.04");
344 assert_eq!(build_config.script, "echo 'Hello World'");
345 assert_eq!(build_config.timeout_seconds, 300);
346 assert_eq!(build_config.compute_type, ComputeType::Medium);
347 assert!(build_config.monitoring.is_some());
348
349 let monitoring = build_config.monitoring.unwrap();
350 assert_eq!(
351 monitoring.endpoint,
352 "https://otel-collector.example.com:4318"
353 );
354 assert_eq!(monitoring.logs_uri, "/v1/logs");
355 assert!(monitoring.tls_enabled);
356 assert!(!monitoring.tls_verify);
357 assert_eq!(monitoring.headers.len(), 2);
358 assert_eq!(
359 monitoring.headers.get("Authorization"),
360 Some(&"Bearer token123".to_string())
361 );
362 assert_eq!(
363 monitoring.headers.get("X-Custom-Header"),
364 Some(&"custom-value".to_string())
365 );
366 }
367
368 #[test]
369 fn test_monitoring_config_defaults() {
370 let monitoring_config = MonitoringConfig {
371 endpoint: "https://otel-collector.example.com:4318".to_string(),
372 headers: HashMap::new(),
373 logs_uri: "/v1/logs".to_string(),
374 tls_enabled: true,
375 tls_verify: true,
376 };
377
378 assert_eq!(monitoring_config.logs_uri, "/v1/logs");
379 assert!(monitoring_config.tls_enabled);
380 assert!(monitoring_config.tls_verify);
381 assert!(monitoring_config.headers.is_empty());
382 }
383}