use crate::error::{ErrorData, Result};
use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef, ResourceType};
use alien_error::AlienError;
use bon::Builder;
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::collections::HashMap;
use std::fmt::Debug;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum BuildStatus {
Queued,
Running,
Succeeded,
Failed,
Cancelled,
TimedOut,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "kebab-case")]
pub enum ComputeType {
Small,
Medium,
Large,
XLarge,
}
impl Default for ComputeType {
fn default() -> Self {
ComputeType::Medium
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct MonitoringConfig {
pub endpoint: String,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default = "default_logs_uri")]
pub logs_uri: String,
#[serde(default = "default_tls_enabled")]
pub tls_enabled: bool,
#[serde(default = "default_tls_verify")]
pub tls_verify: bool,
}
fn default_logs_uri() -> String {
"/v1/logs".to_string()
}
fn default_tls_enabled() -> bool {
true
}
fn default_tls_verify() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct BuildConfig {
pub image: String,
pub script: String,
#[serde(default)]
pub environment: HashMap<String, String>,
pub timeout_seconds: u32,
#[serde(default)]
pub compute_type: ComputeType,
#[serde(skip_serializing_if = "Option::is_none")]
pub monitoring: Option<MonitoringConfig>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[builder(start_fn = new)]
pub struct Build {
#[builder(start_fn)]
pub id: String,
#[builder(field)]
pub links: Vec<ResourceRef>,
pub permissions: String,
#[builder(default)]
#[serde(default)]
pub environment: HashMap<String, String>,
#[builder(default)]
#[serde(default)]
pub compute_type: ComputeType,
}
impl Build {
pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("build");
pub fn get_permissions(&self) -> &str {
&self.permissions
}
}
use crate::resources::build::build_builder::State;
impl<S: State> BuildBuilder<S> {
pub fn link<R: ?Sized>(mut self, resource: &R) -> Self
where
for<'a> &'a R: Into<ResourceRef>, {
let resource_ref: ResourceRef = resource.into();
self.links.push(resource_ref);
self
}
}
impl ResourceDefinition for Build {
fn get_resource_type(&self) -> ResourceType {
Self::RESOURCE_TYPE
}
fn id(&self) -> &str {
&self.id
}
fn get_dependencies(&self) -> Vec<ResourceRef> {
self.links.clone()
}
fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
let new_build = new_config.as_any().downcast_ref::<Build>().ok_or_else(|| {
AlienError::new(ErrorData::UnexpectedResourceType {
resource_id: self.id.clone(),
expected: Self::RESOURCE_TYPE,
actual: new_config.get_resource_type(),
})
})?;
if self.id != new_build.id {
return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
resource_id: self.id.clone(),
reason: "the 'id' field is immutable".to_string(),
}));
}
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn box_clone(&self) -> Box<dyn ResourceDefinition> {
Box::new(self.clone())
}
fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
other.as_any().downcast_ref::<Build>() == Some(self)
}
fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
serde_json::to_value(self)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct BuildOutputs {
pub identifier: String,
}
impl ResourceOutputsDefinition for BuildOutputs {
fn get_resource_type(&self) -> ResourceType {
Build::RESOURCE_TYPE.clone()
}
fn as_any(&self) -> &dyn Any {
self
}
fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
Box::new(self.clone())
}
fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
other.as_any().downcast_ref::<BuildOutputs>() == Some(self)
}
fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
serde_json::to_value(self)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct BuildExecution {
pub id: String,
pub status: BuildStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_time: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Storage;
#[test]
fn test_build_builder_direct_refs() {
let dummy_storage = Storage::new("test-storage".to_string()).build();
let build = Build::new("my-build".to_string())
.permissions("build-execution".to_string())
.link(&dummy_storage) .compute_type(ComputeType::Large)
.build();
assert_eq!(build.id, "my-build");
assert_eq!(build.compute_type, ComputeType::Large);
assert_eq!(build.permissions, "build-execution");
assert!(build
.links
.contains(&ResourceRef::new(Storage::RESOURCE_TYPE, "test-storage")));
assert_eq!(build.links.len(), 1);
}
#[test]
fn test_build_defaults() {
let build = Build::new("default-build".to_string())
.permissions("build-execution".to_string())
.build();
assert_eq!(build.compute_type, ComputeType::Medium);
assert!(build.environment.is_empty());
assert!(build.links.is_empty());
}
#[test]
fn test_build_with_environment() {
let mut env = HashMap::new();
env.insert("NODE_ENV".to_string(), "production".to_string());
env.insert("API_KEY".to_string(), "secret".to_string());
let build = Build::new("env-build".to_string())
.environment(env.clone())
.permissions("test".to_string())
.build();
assert_eq!(build.environment, env);
}
#[test]
fn test_build_config_with_monitoring() {
let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), "Bearer token123".to_string());
headers.insert("X-Custom-Header".to_string(), "custom-value".to_string());
let monitoring_config = MonitoringConfig {
endpoint: "https://otel-collector.example.com:4318".to_string(),
headers,
logs_uri: "/v1/logs".to_string(),
tls_enabled: true,
tls_verify: false,
};
let build_config = BuildConfig {
image: "ubuntu:20.04".to_string(),
script: "echo 'Hello World'".to_string(),
environment: HashMap::new(),
timeout_seconds: 300,
compute_type: ComputeType::Medium,
monitoring: Some(monitoring_config.clone()),
};
assert_eq!(build_config.image, "ubuntu:20.04");
assert_eq!(build_config.script, "echo 'Hello World'");
assert_eq!(build_config.timeout_seconds, 300);
assert_eq!(build_config.compute_type, ComputeType::Medium);
assert!(build_config.monitoring.is_some());
let monitoring = build_config.monitoring.unwrap();
assert_eq!(
monitoring.endpoint,
"https://otel-collector.example.com:4318"
);
assert_eq!(monitoring.logs_uri, "/v1/logs");
assert!(monitoring.tls_enabled);
assert!(!monitoring.tls_verify);
assert_eq!(monitoring.headers.len(), 2);
assert_eq!(
monitoring.headers.get("Authorization"),
Some(&"Bearer token123".to_string())
);
assert_eq!(
monitoring.headers.get("X-Custom-Header"),
Some(&"custom-value".to_string())
);
}
#[test]
fn test_monitoring_config_defaults() {
let monitoring_config = MonitoringConfig {
endpoint: "https://otel-collector.example.com:4318".to_string(),
headers: HashMap::new(),
logs_uri: "/v1/logs".to_string(),
tls_enabled: true,
tls_verify: true,
};
assert_eq!(monitoring_config.logs_uri, "/v1/logs");
assert!(monitoring_config.tls_enabled);
assert!(monitoring_config.tls_verify);
assert!(monitoring_config.headers.is_empty());
}
}