Skip to main content

alien_core/
resource.rs

1use crate::error::Result;
2use serde::{Deserialize, Serialize};
3use std::any::Any;
4use std::borrow::Cow;
5use std::fmt::Debug;
6#[cfg(feature = "openapi")]
7use utoipa::openapi::schema::AdditionalProperties;
8#[cfg(feature = "openapi")]
9use utoipa::openapi::{ObjectBuilder, Ref, RefOr, Schema, Type};
10#[cfg(feature = "openapi")]
11use utoipa::{PartialSchema, ToSchema};
12
13/// Type alias for resource type identifiers
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct ResourceType(pub Cow<'static, str>);
17
18impl ResourceType {
19    /// Create a new ResourceType from a static string (const-friendly)
20    pub const fn from_static(s: &'static str) -> Self {
21        Self(Cow::Borrowed(s))
22    }
23}
24
25impl From<String> for ResourceType {
26    fn from(s: String) -> Self {
27        Self(Cow::Owned(s))
28    }
29}
30
31impl From<&str> for ResourceType {
32    fn from(s: &str) -> Self {
33        Self(Cow::Owned(s.to_string()))
34    }
35}
36
37impl std::fmt::Display for ResourceType {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(f, "{}", self.0)
40    }
41}
42
43impl From<ResourceType> for String {
44    fn from(val: ResourceType) -> Self {
45        val.0.into_owned()
46    }
47}
48
49impl AsRef<str> for ResourceType {
50    fn as_ref(&self) -> &str {
51        &self.0
52    }
53}
54
55#[cfg(feature = "openapi")]
56impl PartialSchema for ResourceType {
57    fn schema() -> RefOr<Schema> {
58        RefOr::T(Schema::Object(
59            ObjectBuilder::new()
60                .schema_type(Type::String)
61                .description(Some("Resource type identifier that determines the specific kind of resource. This field is used for polymorphic deserialization and resource-specific behavior."))
62                .examples([
63                    "function", 
64                    "storage", 
65                    "queue", 
66                    "redis", 
67                    "postgres"
68                ])
69                .build()
70        ))
71    }
72}
73
74#[cfg(feature = "openapi")]
75impl ToSchema for ResourceType {
76    fn name() -> std::borrow::Cow<'static, str> {
77        std::borrow::Cow::Borrowed("ResourceType")
78    }
79}
80
81/// Trait that defines the interface for all resource types in the Alien system.
82/// This trait enables extensibility by allowing new resource types to be registered
83/// and managed alongside built-in resources.
84#[typetag::serde(tag = "type")]
85pub trait ResourceDefinition: Debug + Send + Sync + 'static {
86    /// Returns the static type identifier for this resource type (e.g., "Function", "Storage")
87    fn resource_type() -> ResourceType
88    where
89        Self: Sized;
90
91    /// Returns the resource type for this instance (calls the static method)
92    fn get_resource_type(&self) -> ResourceType;
93
94    /// Returns the unique identifier for this specific resource instance
95    fn id(&self) -> &str;
96
97    /// Returns the list of other resources this resource depends on
98    fn get_dependencies(&self) -> Vec<ResourceRef>;
99
100    /// Returns the permission profile name for this resource, if it has one.
101    ///
102    /// Used by `ServiceAccountDependenciesMutation` to wire the corresponding
103    /// `{profile}-sa` service account as a declared dependency so the executor
104    /// enforces ordering and propagates SA changes automatically.
105    ///
106    /// Override in concrete types that carry a `permissions` field (Container, Function).
107    fn get_permissions(&self) -> Option<&str> {
108        None
109    }
110
111    /// Validates if an update from the current configuration to a new configuration is allowed
112    fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()>;
113
114    /// Provides access to the underlying concrete type for downcasting
115    fn as_any(&self) -> &dyn Any;
116
117    /// Provides mutable access to the underlying concrete type for downcasting
118    fn as_any_mut(&mut self) -> &mut dyn Any;
119
120    /// Creates a boxed clone of this resource definition
121    fn box_clone(&self) -> Box<dyn ResourceDefinition>;
122
123    /// For equality comparison between resource definitions
124    fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool;
125}
126
127/// Clone implementation for boxed ResourceDefinition trait objects
128impl Clone for Box<dyn ResourceDefinition> {
129    fn clone(&self) -> Self {
130        self.box_clone()
131    }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct Resource {
136    #[serde(flatten)]
137    inner: Box<dyn ResourceDefinition>,
138}
139
140impl Resource {
141    /// Creates a new Resource from any type that implements ResourceDefinition
142    pub fn new<T: ResourceDefinition>(resource: T) -> Self {
143        Self {
144            inner: Box::new(resource),
145        }
146    }
147
148    /// Creates a new Resource from a boxed ResourceDefinition
149    pub fn from_boxed(boxed_resource: Box<dyn ResourceDefinition>) -> Self {
150        Self {
151            inner: boxed_resource,
152        }
153    }
154
155    /// Returns the resource type identifier
156    pub fn resource_type(&self) -> ResourceType {
157        self.inner.get_resource_type()
158    }
159
160    /// Returns the unique identifier for this resource instance
161    pub fn id(&self) -> &str {
162        self.inner.id()
163    }
164
165    /// Returns the list of other resources this resource depends on
166    pub fn get_dependencies(&self) -> Vec<ResourceRef> {
167        self.inner.get_dependencies()
168    }
169
170    /// Returns the permission profile name for this resource, if it has one.
171    pub fn get_permissions(&self) -> Option<&str> {
172        self.inner.get_permissions()
173    }
174
175    /// Validates if an update from the current configuration to a new configuration is allowed
176    pub fn validate_update(&self, new_config: &Resource) -> Result<()> {
177        self.inner.validate_update(new_config.inner.as_ref())
178    }
179
180    /// Provides access to the underlying ResourceDefinition trait object
181    pub fn as_resource_definition(&self) -> &dyn ResourceDefinition {
182        self.inner.as_ref()
183    }
184
185    /// Generic downcasting for any type
186    pub fn downcast_ref<T: ResourceDefinition + 'static>(&self) -> Option<&T> {
187        self.inner.as_any().downcast_ref::<T>()
188    }
189
190    /// Generic mutable downcasting for any type
191    pub fn downcast_mut<T: ResourceDefinition + 'static>(&mut self) -> Option<&mut T> {
192        self.inner.as_any_mut().downcast_mut::<T>()
193    }
194}
195
196impl PartialEq for Resource {
197    fn eq(&self, other: &Self) -> bool {
198        self.inner.resource_eq(other.inner.as_ref())
199    }
200}
201
202impl Eq for Resource {}
203
204/// OpenAPI schema implementation for Resource.
205///
206/// The schema represents the flattened JSON structure of any resource type in the Alien system.
207/// All resources have a common base structure with `type` and `id` fields, plus type-specific
208/// additional properties that vary depending on the concrete resource implementation.
209///
210/// # Schema Structure
211/// - `type` (required): The resource type identifier (e.g., "function", "storage", "queue")
212/// - `id` (required): The unique identifier for this specific resource instance  
213/// - Additional properties: Type-specific fields that vary by resource type (e.g., Function has `code`, `memory_mb`, etc.)
214///
215/// # Example JSON
216/// ```json
217/// {
218///   "type": "function",
219///   "id": "my-function",
220///   "code": { "type": "image", "image": "my-image:latest" },
221///   "memoryMb": 512,
222///   "timeoutSeconds": 30
223/// }
224/// ```
225#[cfg(feature = "openapi")]
226impl PartialSchema for Resource {
227    fn schema() -> RefOr<Schema> {
228        RefOr::T(Schema::Object(
229            ObjectBuilder::new()
230                .schema_type(Type::Object)
231                .property("type", Ref::from_schema_name("ResourceType"))
232                .property("id", 
233                    ObjectBuilder::new()
234                        .schema_type(Type::String)
235                        .description(Some("The unique identifier for this specific resource instance. Must contain only alphanumeric characters, hyphens, and underscores ([A-Za-z0-9-_]). Maximum 64 characters."))
236                        .build()
237                )
238                .required("type")
239                .required("id")
240                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
241                .description(Some("Resource that can hold any resource type in the Alien system. All resources share common 'type' and 'id' fields with additional type-specific properties."))
242                .build()
243        ))
244    }
245}
246
247#[cfg(feature = "openapi")]
248impl ToSchema for Resource {
249    fn name() -> std::borrow::Cow<'static, str> {
250        std::borrow::Cow::Borrowed("BaseResource")
251    }
252}
253
254/// New ResourceRef that works with any resource type.
255/// This can eventually replace the enum-based ResourceRef for full extensibility.
256#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
257#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
258#[serde(rename_all = "camelCase")]
259pub struct ResourceRef {
260    #[serde(rename = "type")]
261    pub resource_type: ResourceType,
262    pub id: String,
263}
264
265impl ResourceRef {
266    /// Creates a new ResourceRef
267    pub fn new(resource_type: ResourceType, id: impl Into<String>) -> Self {
268        Self {
269            resource_type,
270            id: id.into(),
271        }
272    }
273
274    /// Returns the resource type
275    pub fn resource_type(&self) -> &ResourceType {
276        &self.resource_type
277    }
278
279    /// Returns the resource id
280    pub fn id(&self) -> &str {
281        &self.id
282    }
283}
284
285impl<T: ResourceDefinition> From<&T> for ResourceRef {
286    fn from(resource: &T) -> Self {
287        Self::new(T::resource_type(), resource.id())
288    }
289}
290
291impl From<&Resource> for ResourceRef {
292    fn from(resource: &Resource) -> Self {
293        Self::new(resource.resource_type(), resource.id())
294    }
295}
296
297/// Trait that defines the interface for all resource output types in the Alien system.
298/// This trait enables extensibility by allowing new resource output types to be registered
299/// and managed alongside built-in resource outputs.
300#[typetag::serde(tag = "type")]
301pub trait ResourceOutputsDefinition: Debug + Send + Sync + 'static {
302    /// Returns the resource type this output corresponds to
303    fn resource_type() -> ResourceType
304    where
305        Self: Sized;
306
307    /// Provides access to the underlying concrete type for downcasting
308    fn as_any(&self) -> &dyn Any;
309
310    /// Creates a boxed clone of this resource outputs
311    fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition>;
312
313    /// For equality comparison between resource outputs
314    fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool;
315}
316
317/// Clone implementation for boxed ResourceOutputsDefinition trait objects
318impl Clone for Box<dyn ResourceOutputsDefinition> {
319    fn clone(&self) -> Self {
320        self.box_clone()
321    }
322}
323
324/// New Resource outputs wrapper that can hold any ResourceOutputsDefinition.
325/// This replaces the old ResourceOutputs enum to enable runtime extensibility.
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct ResourceOutputs {
328    #[serde(flatten)]
329    inner: Box<dyn ResourceOutputsDefinition>,
330}
331
332impl ResourceOutputs {
333    /// Creates a new ResourceOutputs from any type that implements ResourceOutputsDefinition
334    pub fn new<T: ResourceOutputsDefinition>(outputs: T) -> Self {
335        Self {
336            inner: Box::new(outputs),
337        }
338    }
339
340    /// Provides access to the underlying ResourceOutputsDefinition trait object
341    pub fn as_resource_outputs(&self) -> &dyn ResourceOutputsDefinition {
342        self.inner.as_ref()
343    }
344
345    /// Generic downcasting for any type
346    pub fn downcast_ref<T: ResourceOutputsDefinition + 'static>(&self) -> Option<&T> {
347        self.inner.as_any().downcast_ref::<T>()
348    }
349}
350
351impl PartialEq for ResourceOutputs {
352    fn eq(&self, other: &Self) -> bool {
353        self.inner.outputs_eq(other.inner.as_ref())
354    }
355}
356
357impl Eq for ResourceOutputs {}
358
359/// OpenAPI schema implementation for ResourceOutputs.
360///
361/// The schema represents the flattened JSON structure of any resource outputs in the Alien system.
362/// All resource outputs have a common base structure with a `type` field, plus type-specific
363/// additional properties that vary depending on the concrete resource implementation.
364///
365/// # Schema Structure
366/// - `type` (required): The resource type identifier (e.g., "function", "storage", "queue")
367/// - Additional properties: Type-specific output fields that vary by resource type
368///
369/// # Example JSON
370/// ```json
371/// {
372///   "type": "function",
373///   "functionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-function",
374///   "functionUrl": "https://abc123.lambda-url.us-east-1.on.aws/"
375/// }
376/// ```
377#[cfg(feature = "openapi")]
378impl PartialSchema for ResourceOutputs {
379    fn schema() -> RefOr<Schema> {
380        RefOr::T(Schema::Object(
381            ObjectBuilder::new()
382                .schema_type(Type::Object)
383                .property("type", Ref::from_schema_name("ResourceType"))
384                .required("type")
385                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
386                .description(Some("Resource outputs that can hold output data for any resource type in the Alien system. All resource outputs share a common 'type' field with additional type-specific output properties."))
387                .build()
388        ))
389    }
390}
391
392#[cfg(feature = "openapi")]
393impl ToSchema for ResourceOutputs {
394    fn name() -> std::borrow::Cow<'static, str> {
395        std::borrow::Cow::Borrowed("BaseResourceOutputs")
396    }
397}
398
399/// Represents the high-level status of a resource during its lifecycle.
400#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
401#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
402#[serde(rename_all = "kebab-case")]
403pub enum ResourceStatus {
404    Pending,      // Initial state before any action starts
405    Provisioning, // Resource is being created or updated
406    ProvisionFailed,
407    Running, // Resource is active and configured as desired
408    Updating,
409    UpdateFailed,
410    Deleting, // Resource is being removed
411    DeleteFailed,
412    Deleted,       // Resource has been successfully removed (terminal state)
413    RefreshFailed, // Resource heartbeat/health check failed
414}
415
416impl ResourceStatus {
417    pub fn is_terminal(&self) -> bool {
418        match self {
419            ResourceStatus::Deleted => true,
420            ResourceStatus::ProvisionFailed => true,
421            ResourceStatus::UpdateFailed => true,
422            ResourceStatus::DeleteFailed => true,
423            ResourceStatus::RefreshFailed => true,
424            _ => false, // Pending, Provisioning, Updating, Deleting are not terminal
425        }
426    }
427}
428
429/// Describes the lifecycle of a resource within a stack, determining how it's managed and deployed.
430#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Hash, Deserialize)]
431#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
432#[serde(rename_all = "kebab-case")]
433pub enum ResourceLifecycle {
434    /// Frozen resources are created once during initial setup and are rarely, if ever, modified.
435    /// They typically require fewer permissions for ongoing management after the initial deployment.
436    /// Example: S3 buckets for logs, VPCs, IAM roles.
437    Frozen,
438
439    /// Live resources are frequently updated as part of ongoing deployments.
440    /// They generally require more permissions for ongoing management to allow for these frequent updates.
441    /// By default, live resources are not created during the initial setup phase unless specified.
442    /// Example: Lambda functions, Cloud Run services.
443    Live,
444
445    /// LiveOnSetup resources are live resources that are specifically designated to be created
446    /// during the initial setup phase of the stack. This is useful for resources that need to be
447    /// present from the beginning but are still expected to be updated frequently.
448    /// Example: A managing function that orchestrates updates for other live resources.
449    LiveOnSetup,
450}
451
452impl ResourceLifecycle {
453    /// Returns `true` if the resource is considered live (i.e., `Live` or `LiveOnSetup`).
454    pub fn is_live(&self) -> bool {
455        match self {
456            ResourceLifecycle::Frozen => false,
457            ResourceLifecycle::Live | ResourceLifecycle::LiveOnSetup => true,
458        }
459    }
460
461    /// Returns `true` if the resource should be created or configured during the initial setup phase.
462    /// This applies to `Frozen` resources and `LiveOnSetup` resources.
463    pub fn initial_setup(&self) -> bool {
464        match self {
465            ResourceLifecycle::Frozen | ResourceLifecycle::LiveOnSetup => true,
466            ResourceLifecycle::Live => false,
467        }
468    }
469}