Skip to main content

alien_core/resources/
daemon.rs

1use crate::error::{ErrorData, Result};
2use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef, ResourceType};
3use crate::resources::{ComputeCluster, ResourceSpec, ToolchainConfig};
4use alien_error::AlienError;
5use bon::Builder;
6use serde::{Deserialize, Serialize};
7use std::any::Any;
8use std::collections::HashMap;
9use std::fmt::Debug;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[serde(rename_all = "camelCase", tag = "type")]
14pub enum DaemonCode {
15    #[serde(rename_all = "camelCase")]
16    Image { image: String },
17    #[serde(rename_all = "camelCase")]
18    Source {
19        src: String,
20        toolchain: ToolchainConfig,
21    },
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
25#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27#[builder(start_fn = new)]
28pub struct Daemon {
29    #[builder(start_fn)]
30    pub id: String,
31    #[builder(field)]
32    pub links: Vec<ResourceRef>,
33    /// ComputeCluster resource ID that this daemon runs on for Horizon-backed
34    /// cloud platforms. Kubernetes and Local runtimes ignore this field.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub cluster: Option<String>,
37    pub permissions: String,
38    pub code: DaemonCode,
39    /// CPU resource requirements for each daemon instance.
40    #[builder(default = default_daemon_cpu())]
41    #[serde(default = "default_daemon_cpu")]
42    pub cpu: ResourceSpec,
43    /// Memory resource requirements for each daemon instance.
44    #[builder(default = default_daemon_memory())]
45    #[serde(default = "default_daemon_memory")]
46    pub memory: ResourceSpec,
47    /// Capacity group/pool to run on for backends that expose machine pools.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub pool: Option<String>,
50    /// Command to override the image default.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub command: Option<Vec<String>>,
53    #[builder(default)]
54    #[serde(default)]
55    pub environment: HashMap<String, String>,
56    #[builder(default = default_commands_enabled())]
57    #[serde(default = "default_commands_enabled")]
58    #[cfg_attr(feature = "openapi", schema(default = default_commands_enabled))]
59    pub commands_enabled: bool,
60}
61
62impl Daemon {
63    pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("daemon");
64
65    pub fn get_permissions(&self) -> &str {
66        &self.permissions
67    }
68}
69
70fn default_commands_enabled() -> bool {
71    false
72}
73
74fn default_daemon_cpu() -> ResourceSpec {
75    ResourceSpec {
76        min: "0.1".to_string(),
77        desired: "0.1".to_string(),
78    }
79}
80
81fn default_daemon_memory() -> ResourceSpec {
82    ResourceSpec {
83        min: "128Mi".to_string(),
84        desired: "128Mi".to_string(),
85    }
86}
87
88impl<S: daemon_builder::State> DaemonBuilder<S> {
89    pub fn link<R: ?Sized>(mut self, resource: &R) -> Self
90    where
91        for<'a> &'a R: Into<ResourceRef>,
92    {
93        let resource_ref: ResourceRef = resource.into();
94        self.links.push(resource_ref);
95        self
96    }
97}
98
99impl ResourceDefinition for Daemon {
100    fn get_resource_type(&self) -> ResourceType {
101        Self::RESOURCE_TYPE
102    }
103
104    fn id(&self) -> &str {
105        &self.id
106    }
107
108    fn get_dependencies(&self) -> Vec<ResourceRef> {
109        let mut dependencies = self.links.clone();
110        if let Some(cluster) = &self.cluster {
111            dependencies.push(ResourceRef::new(
112                ComputeCluster::RESOURCE_TYPE,
113                cluster.clone(),
114            ));
115        }
116        dependencies
117    }
118
119    fn get_permissions(&self) -> Option<&str> {
120        Some(&self.permissions)
121    }
122
123    fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
124        let new_daemon = new_config
125            .as_any()
126            .downcast_ref::<Daemon>()
127            .ok_or_else(|| {
128                AlienError::new(ErrorData::UnexpectedResourceType {
129                    resource_id: self.id.clone(),
130                    expected: Self::RESOURCE_TYPE,
131                    actual: new_config.get_resource_type(),
132                })
133            })?;
134
135        if self.id != new_daemon.id {
136            return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
137                resource_id: self.id.clone(),
138                reason: "the 'id' field is immutable".to_string(),
139            }));
140        }
141
142        Ok(())
143    }
144
145    fn as_any(&self) -> &dyn Any {
146        self
147    }
148
149    fn as_any_mut(&mut self) -> &mut dyn Any {
150        self
151    }
152
153    fn box_clone(&self) -> Box<dyn ResourceDefinition> {
154        Box::new(self.clone())
155    }
156
157    fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
158        other.as_any().downcast_ref::<Daemon>() == Some(self)
159    }
160
161    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
162        serde_json::to_value(self)
163    }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
168#[serde(rename_all = "camelCase")]
169pub struct DaemonOutputs {
170    pub daemon_name: String,
171    pub running: bool,
172}
173
174impl ResourceOutputsDefinition for DaemonOutputs {
175    fn get_resource_type(&self) -> ResourceType {
176        Daemon::RESOURCE_TYPE.clone()
177    }
178
179    fn as_any(&self) -> &dyn Any {
180        self
181    }
182
183    fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
184        Box::new(self.clone())
185    }
186
187    fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
188        other.as_any().downcast_ref::<DaemonOutputs>() == Some(self)
189    }
190
191    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
192        serde_json::to_value(self)
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn daemon_serializes_with_resource_type() {
202        let daemon = Daemon::new("endpoint-agent".to_string())
203            .code(DaemonCode::Source {
204                src: "./agent".to_string(),
205                toolchain: ToolchainConfig::Rust {
206                    binary_name: "agent".to_string(),
207                },
208            })
209            .permissions("execution".to_string())
210            .commands_enabled(true)
211            .build();
212
213        let resource = crate::Resource::new(daemon);
214        let json = serde_json::to_value(&resource).expect("daemon should serialize");
215        assert_eq!(json["type"], "daemon");
216
217        let roundtrip: crate::Resource =
218            serde_json::from_value(json).expect("daemon should deserialize");
219        assert_eq!(roundtrip.resource_type().as_ref(), "daemon");
220    }
221}