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
163#[typetag::serde(name = "build")]
165impl ResourceDefinition for Build {
166 fn resource_type() -> ResourceType {
167 Self::RESOURCE_TYPE.clone()
168 }
169
170 fn get_resource_type(&self) -> ResourceType {
171 Self::resource_type()
172 }
173
174 fn id(&self) -> &str {
175 &self.id
176 }
177
178 fn get_dependencies(&self) -> Vec<ResourceRef> {
179 self.links.clone()
182 }
183
184 fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
185 let new_build = new_config.as_any().downcast_ref::<Build>().ok_or_else(|| {
187 AlienError::new(ErrorData::UnexpectedResourceType {
188 resource_id: self.id.clone(),
189 expected: Self::RESOURCE_TYPE,
190 actual: new_config.get_resource_type(),
191 })
192 })?;
193
194 if self.id != new_build.id {
195 return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
196 resource_id: self.id.clone(),
197 reason: "the 'id' field is immutable".to_string(),
198 }));
199 }
200 Ok(())
201 }
202
203 fn as_any(&self) -> &dyn Any {
204 self
205 }
206
207 fn as_any_mut(&mut self) -> &mut dyn Any {
208 self
209 }
210
211 fn box_clone(&self) -> Box<dyn ResourceDefinition> {
212 Box::new(self.clone())
213 }
214
215 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
216 other.as_any().downcast_ref::<Build>() == Some(self)
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
222#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
223#[serde(rename_all = "camelCase")]
224pub struct BuildOutputs {
225 pub identifier: String,
227}
228
229#[typetag::serde(name = "build")]
230impl ResourceOutputsDefinition for BuildOutputs {
231 fn resource_type() -> ResourceType {
232 Build::RESOURCE_TYPE.clone()
233 }
234
235 fn as_any(&self) -> &dyn Any {
236 self
237 }
238
239 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
240 Box::new(self.clone())
241 }
242
243 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
244 other.as_any().downcast_ref::<BuildOutputs>() == Some(self)
245 }
246}
247
248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
250#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
251#[serde(rename_all = "camelCase")]
252pub struct BuildExecution {
253 pub id: String,
255 pub status: BuildStatus,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub start_time: Option<String>,
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub end_time: Option<String>,
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::Storage;
269
270 #[test]
271 fn test_build_builder_direct_refs() {
272 let dummy_storage = Storage::new("test-storage".to_string()).build();
273
274 let build = Build::new("my-build".to_string())
275 .permissions("build-execution".to_string())
276 .link(&dummy_storage) .compute_type(ComputeType::Large)
278 .build();
279
280 assert_eq!(build.id, "my-build");
281 assert_eq!(build.compute_type, ComputeType::Large);
282
283 assert_eq!(build.permissions, "build-execution");
285
286 assert!(build
288 .links
289 .contains(&ResourceRef::new(Storage::RESOURCE_TYPE, "test-storage")));
290 assert_eq!(build.links.len(), 1);
291 }
292
293 #[test]
294 fn test_build_defaults() {
295 let build = Build::new("default-build".to_string())
296 .permissions("build-execution".to_string())
297 .build();
298
299 assert_eq!(build.compute_type, ComputeType::Medium);
300 assert!(build.environment.is_empty());
301 assert!(build.links.is_empty());
302 }
303
304 #[test]
305 fn test_build_with_environment() {
306 let mut env = HashMap::new();
307 env.insert("NODE_ENV".to_string(), "production".to_string());
308 env.insert("API_KEY".to_string(), "secret".to_string());
309
310 let build = Build::new("env-build".to_string())
311 .environment(env.clone())
312 .permissions("test".to_string())
313 .build();
314
315 assert_eq!(build.environment, env);
316 }
317
318 #[test]
319 fn test_build_config_with_monitoring() {
320 let mut headers = HashMap::new();
321 headers.insert("Authorization".to_string(), "Bearer token123".to_string());
322 headers.insert("X-Custom-Header".to_string(), "custom-value".to_string());
323
324 let monitoring_config = MonitoringConfig {
325 endpoint: "https://otel-collector.example.com:4318".to_string(),
326 headers,
327 logs_uri: "/v1/logs".to_string(),
328 tls_enabled: true,
329 tls_verify: false,
330 };
331
332 let build_config = BuildConfig {
333 image: "ubuntu:20.04".to_string(),
334 script: "echo 'Hello World'".to_string(),
335 environment: HashMap::new(),
336 timeout_seconds: 300,
337 compute_type: ComputeType::Medium,
338 monitoring: Some(monitoring_config.clone()),
339 };
340
341 assert_eq!(build_config.image, "ubuntu:20.04");
342 assert_eq!(build_config.script, "echo 'Hello World'");
343 assert_eq!(build_config.timeout_seconds, 300);
344 assert_eq!(build_config.compute_type, ComputeType::Medium);
345 assert!(build_config.monitoring.is_some());
346
347 let monitoring = build_config.monitoring.unwrap();
348 assert_eq!(
349 monitoring.endpoint,
350 "https://otel-collector.example.com:4318"
351 );
352 assert_eq!(monitoring.logs_uri, "/v1/logs");
353 assert!(monitoring.tls_enabled);
354 assert!(!monitoring.tls_verify);
355 assert_eq!(monitoring.headers.len(), 2);
356 assert_eq!(
357 monitoring.headers.get("Authorization"),
358 Some(&"Bearer token123".to_string())
359 );
360 assert_eq!(
361 monitoring.headers.get("X-Custom-Header"),
362 Some(&"custom-value".to_string())
363 );
364 }
365
366 #[test]
367 fn test_monitoring_config_defaults() {
368 let monitoring_config = MonitoringConfig {
369 endpoint: "https://otel-collector.example.com:4318".to_string(),
370 headers: HashMap::new(),
371 logs_uri: "/v1/logs".to_string(),
372 tls_enabled: true,
373 tls_verify: true,
374 };
375
376 assert_eq!(monitoring_config.logs_uri, "/v1/logs");
377 assert!(monitoring_config.tls_enabled);
378 assert!(monitoring_config.tls_verify);
379 assert!(monitoring_config.headers.is_empty());
380 }
381}