alien_core/resources/
daemon.rs1use 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 #[serde(skip_serializing_if = "Option::is_none")]
36 pub cluster: Option<String>,
37 pub permissions: String,
38 pub code: DaemonCode,
39 #[builder(default = default_daemon_cpu())]
41 #[serde(default = "default_daemon_cpu")]
42 pub cpu: ResourceSpec,
43 #[builder(default = default_daemon_memory())]
45 #[serde(default = "default_daemon_memory")]
46 pub memory: ResourceSpec,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub pool: Option<String>,
50 #[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}