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.
84pub trait ResourceDefinition: Debug + Send + Sync + 'static {
85    /// Returns the resource type for this instance
86    fn get_resource_type(&self) -> ResourceType;
87
88    /// Returns the unique identifier for this specific resource instance
89    fn id(&self) -> &str;
90
91    /// Returns the list of other resources this resource depends on
92    fn get_dependencies(&self) -> Vec<ResourceRef>;
93
94    /// Returns the permission profile name for this resource, if it has one.
95    ///
96    /// Used by `ServiceAccountDependenciesMutation` to wire the corresponding
97    /// `{profile}-sa` service account as a declared dependency so the executor
98    /// enforces ordering and propagates SA changes automatically.
99    ///
100    /// Override in concrete types that carry a `permissions` field (Container, Function).
101    fn get_permissions(&self) -> Option<&str> {
102        None
103    }
104
105    /// Validates if an update from the current configuration to a new configuration is allowed
106    fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()>;
107
108    /// Provides access to the underlying concrete type for downcasting
109    fn as_any(&self) -> &dyn Any;
110
111    /// Provides mutable access to the underlying concrete type for downcasting
112    fn as_any_mut(&mut self) -> &mut dyn Any;
113
114    /// Creates a boxed clone of this resource definition
115    fn box_clone(&self) -> Box<dyn ResourceDefinition>;
116
117    /// For equality comparison between resource definitions
118    fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool;
119
120    /// Serialize this resource to a JSON value (without the "type" tag - that's added by Resource)
121    fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
122}
123
124/// Clone implementation for boxed ResourceDefinition trait objects
125impl Clone for Box<dyn ResourceDefinition> {
126    fn clone(&self) -> Self {
127        self.box_clone()
128    }
129}
130
131#[derive(Debug, Clone)]
132pub struct Resource {
133    inner: Box<dyn ResourceDefinition>,
134}
135
136impl Serialize for Resource {
137    fn serialize<S: serde::Serializer>(
138        &self,
139        serializer: S,
140    ) -> std::result::Result<S::Ok, S::Error> {
141        let mut v = self
142            .inner
143            .to_json_value()
144            .map_err(serde::ser::Error::custom)?;
145        v.as_object_mut()
146            .ok_or_else(|| serde::ser::Error::custom("resource must serialize as object"))?
147            .insert(
148                "type".into(),
149                serde_json::Value::String(self.inner.get_resource_type().0.into_owned()),
150            );
151        v.serialize(serializer)
152    }
153}
154
155impl<'de> Deserialize<'de> for Resource {
156    fn deserialize<D: serde::Deserializer<'de>>(
157        deserializer: D,
158    ) -> std::result::Result<Self, D::Error> {
159        let mut value = serde_json::Value::deserialize(deserializer)?;
160        let type_tag = value
161            .get("type")
162            .and_then(|v| v.as_str())
163            .ok_or_else(|| serde::de::Error::missing_field("type"))?
164            .to_string();
165
166        // Remove the "type" tag before passing to concrete deserializer
167        // (structs with deny_unknown_fields would reject it)
168        if let Some(obj) = value.as_object_mut() {
169            obj.remove("type");
170        }
171
172        let inner: Box<dyn ResourceDefinition> = match type_tag.as_str() {
173            "vault" => Box::new(
174                serde_json::from_value::<crate::resources::Vault>(value)
175                    .map_err(serde::de::Error::custom)?,
176            ),
177            "function" => Box::new(
178                serde_json::from_value::<crate::resources::Function>(value)
179                    .map_err(serde::de::Error::custom)?,
180            ),
181            "container" => Box::new(
182                serde_json::from_value::<crate::resources::Container>(value)
183                    .map_err(serde::de::Error::custom)?,
184            ),
185            "container-cluster" => Box::new(
186                serde_json::from_value::<crate::resources::ContainerCluster>(value)
187                    .map_err(serde::de::Error::custom)?,
188            ),
189            "storage" => Box::new(
190                serde_json::from_value::<crate::resources::Storage>(value)
191                    .map_err(serde::de::Error::custom)?,
192            ),
193            "queue" => Box::new(
194                serde_json::from_value::<crate::resources::Queue>(value)
195                    .map_err(serde::de::Error::custom)?,
196            ),
197            "kv" => Box::new(
198                serde_json::from_value::<crate::resources::Kv>(value)
199                    .map_err(serde::de::Error::custom)?,
200            ),
201            "network" => Box::new(
202                serde_json::from_value::<crate::resources::Network>(value)
203                    .map_err(serde::de::Error::custom)?,
204            ),
205            "build" => Box::new(
206                serde_json::from_value::<crate::resources::Build>(value)
207                    .map_err(serde::de::Error::custom)?,
208            ),
209            "service-account" => Box::new(
210                serde_json::from_value::<crate::resources::ServiceAccount>(value)
211                    .map_err(serde::de::Error::custom)?,
212            ),
213            "artifact-registry" => Box::new(
214                serde_json::from_value::<crate::resources::ArtifactRegistry>(value)
215                    .map_err(serde::de::Error::custom)?,
216            ),
217            "service_activation" => Box::new(
218                serde_json::from_value::<crate::resources::ServiceActivation>(value)
219                    .map_err(serde::de::Error::custom)?,
220            ),
221            "remote-stack-management" => Box::new(
222                serde_json::from_value::<crate::resources::RemoteStackManagement>(value)
223                    .map_err(serde::de::Error::custom)?,
224            ),
225            "azure_resource_group" => Box::new(
226                serde_json::from_value::<crate::resources::AzureResourceGroup>(value)
227                    .map_err(serde::de::Error::custom)?,
228            ),
229            "azure_storage_account" => Box::new(
230                serde_json::from_value::<crate::resources::AzureStorageAccount>(value)
231                    .map_err(serde::de::Error::custom)?,
232            ),
233            "azure_container_apps_environment" => Box::new(
234                serde_json::from_value::<crate::resources::AzureContainerAppsEnvironment>(value)
235                    .map_err(serde::de::Error::custom)?,
236            ),
237            "azure_service_bus_namespace" => Box::new(
238                serde_json::from_value::<crate::resources::AzureServiceBusNamespace>(value)
239                    .map_err(serde::de::Error::custom)?,
240            ),
241            other => {
242                return Err(serde::de::Error::unknown_variant(
243                    other,
244                    &[
245                        "vault",
246                        "function",
247                        "container",
248                        "container-cluster",
249                        "storage",
250                        "queue",
251                        "kv",
252                        "network",
253                        "build",
254                        "service-account",
255                        "artifact-registry",
256                        "service_activation",
257                        "remote-stack-management",
258                        "azure_resource_group",
259                        "azure_storage_account",
260                        "azure_container_apps_environment",
261                        "azure_service_bus_namespace",
262                    ],
263                ))
264            }
265        };
266
267        Ok(Resource { inner })
268    }
269}
270
271impl Resource {
272    /// Creates a new Resource from any type that implements ResourceDefinition
273    pub fn new<T: ResourceDefinition>(resource: T) -> Self {
274        Self {
275            inner: Box::new(resource),
276        }
277    }
278
279    /// Creates a new Resource from a boxed ResourceDefinition
280    pub fn from_boxed(boxed_resource: Box<dyn ResourceDefinition>) -> Self {
281        Self {
282            inner: boxed_resource,
283        }
284    }
285
286    /// Returns the resource type identifier
287    pub fn resource_type(&self) -> ResourceType {
288        self.inner.get_resource_type()
289    }
290
291    /// Returns the unique identifier for this resource instance
292    pub fn id(&self) -> &str {
293        self.inner.id()
294    }
295
296    /// Returns the list of other resources this resource depends on
297    pub fn get_dependencies(&self) -> Vec<ResourceRef> {
298        self.inner.get_dependencies()
299    }
300
301    /// Returns the permission profile name for this resource, if it has one.
302    pub fn get_permissions(&self) -> Option<&str> {
303        self.inner.get_permissions()
304    }
305
306    /// Validates if an update from the current configuration to a new configuration is allowed
307    pub fn validate_update(&self, new_config: &Resource) -> Result<()> {
308        self.inner.validate_update(new_config.inner.as_ref())
309    }
310
311    /// Provides access to the underlying ResourceDefinition trait object
312    pub fn as_resource_definition(&self) -> &dyn ResourceDefinition {
313        self.inner.as_ref()
314    }
315
316    /// Generic downcasting for any type
317    pub fn downcast_ref<T: ResourceDefinition + 'static>(&self) -> Option<&T> {
318        self.inner.as_any().downcast_ref::<T>()
319    }
320
321    /// Generic mutable downcasting for any type
322    pub fn downcast_mut<T: ResourceDefinition + 'static>(&mut self) -> Option<&mut T> {
323        self.inner.as_any_mut().downcast_mut::<T>()
324    }
325}
326
327impl PartialEq for Resource {
328    fn eq(&self, other: &Self) -> bool {
329        self.inner.resource_eq(other.inner.as_ref())
330    }
331}
332
333impl Eq for Resource {}
334
335/// OpenAPI schema implementation for Resource.
336///
337/// The schema represents the flattened JSON structure of any resource type in the Alien system.
338/// All resources have a common base structure with `type` and `id` fields, plus type-specific
339/// additional properties that vary depending on the concrete resource implementation.
340///
341/// # Schema Structure
342/// - `type` (required): The resource type identifier (e.g., "function", "storage", "queue")
343/// - `id` (required): The unique identifier for this specific resource instance
344/// - Additional properties: Type-specific fields that vary by resource type (e.g., Function has `code`, `memory_mb`, etc.)
345///
346/// # Example JSON
347/// ```json
348/// {
349///   "type": "function",
350///   "id": "my-function",
351///   "code": { "type": "image", "image": "my-image:latest" },
352///   "memoryMb": 512,
353///   "timeoutSeconds": 30
354/// }
355/// ```
356#[cfg(feature = "openapi")]
357impl PartialSchema for Resource {
358    fn schema() -> RefOr<Schema> {
359        RefOr::T(Schema::Object(
360            ObjectBuilder::new()
361                .schema_type(Type::Object)
362                .property("type", Ref::from_schema_name("ResourceType"))
363                .property("id",
364                    ObjectBuilder::new()
365                        .schema_type(Type::String)
366                        .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."))
367                        .build()
368                )
369                .required("type")
370                .required("id")
371                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
372                .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."))
373                .build()
374        ))
375    }
376}
377
378#[cfg(feature = "openapi")]
379impl ToSchema for Resource {
380    fn name() -> std::borrow::Cow<'static, str> {
381        std::borrow::Cow::Borrowed("BaseResource")
382    }
383}
384
385/// New ResourceRef that works with any resource type.
386/// This can eventually replace the enum-based ResourceRef for full extensibility.
387#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
388#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
389#[serde(rename_all = "camelCase")]
390pub struct ResourceRef {
391    #[serde(rename = "type")]
392    pub resource_type: ResourceType,
393    pub id: String,
394}
395
396impl ResourceRef {
397    /// Creates a new ResourceRef
398    pub fn new(resource_type: ResourceType, id: impl Into<String>) -> Self {
399        Self {
400            resource_type,
401            id: id.into(),
402        }
403    }
404
405    /// Returns the resource type
406    pub fn resource_type(&self) -> &ResourceType {
407        &self.resource_type
408    }
409
410    /// Returns the resource id
411    pub fn id(&self) -> &str {
412        &self.id
413    }
414}
415
416impl<T: ResourceDefinition> From<&T> for ResourceRef {
417    fn from(resource: &T) -> Self {
418        Self::new(resource.get_resource_type(), resource.id())
419    }
420}
421
422impl From<&Resource> for ResourceRef {
423    fn from(resource: &Resource) -> Self {
424        Self::new(resource.resource_type(), resource.id())
425    }
426}
427
428/// Trait that defines the interface for all resource output types in the Alien system.
429/// This trait enables extensibility by allowing new resource output types to be registered
430/// and managed alongside built-in resource outputs.
431pub trait ResourceOutputsDefinition: Debug + Send + Sync + 'static {
432    /// Returns the resource type for this instance
433    fn get_resource_type(&self) -> ResourceType;
434
435    /// Provides access to the underlying concrete type for downcasting
436    fn as_any(&self) -> &dyn Any;
437
438    /// Creates a boxed clone of this resource outputs
439    fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition>;
440
441    /// For equality comparison between resource outputs
442    fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool;
443
444    /// Serialize this resource outputs to a JSON value (without the "type" tag - that's added by ResourceOutputs)
445    fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
446}
447
448/// Clone implementation for boxed ResourceOutputsDefinition trait objects
449impl Clone for Box<dyn ResourceOutputsDefinition> {
450    fn clone(&self) -> Self {
451        self.box_clone()
452    }
453}
454
455/// New Resource outputs wrapper that can hold any ResourceOutputsDefinition.
456/// This replaces the old ResourceOutputs enum to enable runtime extensibility.
457#[derive(Debug, Clone)]
458pub struct ResourceOutputs {
459    inner: Box<dyn ResourceOutputsDefinition>,
460}
461
462impl Serialize for ResourceOutputs {
463    fn serialize<S: serde::Serializer>(
464        &self,
465        serializer: S,
466    ) -> std::result::Result<S::Ok, S::Error> {
467        let mut v = self
468            .inner
469            .to_json_value()
470            .map_err(serde::ser::Error::custom)?;
471        v.as_object_mut()
472            .ok_or_else(|| serde::ser::Error::custom("resource outputs must serialize as object"))?
473            .insert(
474                "type".into(),
475                serde_json::Value::String(self.inner.get_resource_type().0.into_owned()),
476            );
477        v.serialize(serializer)
478    }
479}
480
481impl<'de> Deserialize<'de> for ResourceOutputs {
482    fn deserialize<D: serde::Deserializer<'de>>(
483        deserializer: D,
484    ) -> std::result::Result<Self, D::Error> {
485        let mut value = serde_json::Value::deserialize(deserializer)?;
486        let type_tag = value
487            .get("type")
488            .and_then(|v| v.as_str())
489            .ok_or_else(|| serde::de::Error::missing_field("type"))?
490            .to_string();
491
492        // Remove the "type" tag before passing to concrete deserializer
493        // (structs with deny_unknown_fields would reject it)
494        if let Some(obj) = value.as_object_mut() {
495            obj.remove("type");
496        }
497
498        let inner: Box<dyn ResourceOutputsDefinition> = match type_tag.as_str() {
499            "vault" => Box::new(
500                serde_json::from_value::<crate::resources::VaultOutputs>(value)
501                    .map_err(serde::de::Error::custom)?,
502            ),
503            "function" => Box::new(
504                serde_json::from_value::<crate::resources::FunctionOutputs>(value)
505                    .map_err(serde::de::Error::custom)?,
506            ),
507            "container" => Box::new(
508                serde_json::from_value::<crate::resources::ContainerOutputs>(value)
509                    .map_err(serde::de::Error::custom)?,
510            ),
511            "container-cluster" => Box::new(
512                serde_json::from_value::<crate::resources::ContainerClusterOutputs>(value)
513                    .map_err(serde::de::Error::custom)?,
514            ),
515            "storage" => Box::new(
516                serde_json::from_value::<crate::resources::StorageOutputs>(value)
517                    .map_err(serde::de::Error::custom)?,
518            ),
519            "queue" => Box::new(
520                serde_json::from_value::<crate::resources::QueueOutputs>(value)
521                    .map_err(serde::de::Error::custom)?,
522            ),
523            "kv" => Box::new(
524                serde_json::from_value::<crate::resources::KvOutputs>(value)
525                    .map_err(serde::de::Error::custom)?,
526            ),
527            "network" => Box::new(
528                serde_json::from_value::<crate::resources::NetworkOutputs>(value)
529                    .map_err(serde::de::Error::custom)?,
530            ),
531            "build" => Box::new(
532                serde_json::from_value::<crate::resources::BuildOutputs>(value)
533                    .map_err(serde::de::Error::custom)?,
534            ),
535            "service-account" => Box::new(
536                serde_json::from_value::<crate::resources::ServiceAccountOutputs>(value)
537                    .map_err(serde::de::Error::custom)?,
538            ),
539            "artifact-registry" => Box::new(
540                serde_json::from_value::<crate::resources::ArtifactRegistryOutputs>(value)
541                    .map_err(serde::de::Error::custom)?,
542            ),
543            "service_activation" => Box::new(
544                serde_json::from_value::<crate::resources::ServiceActivationOutputs>(value)
545                    .map_err(serde::de::Error::custom)?,
546            ),
547            "remote-stack-management" => Box::new(
548                serde_json::from_value::<crate::resources::RemoteStackManagementOutputs>(value)
549                    .map_err(serde::de::Error::custom)?,
550            ),
551            "azure_resource_group" => Box::new(
552                serde_json::from_value::<crate::resources::AzureResourceGroupOutputs>(value)
553                    .map_err(serde::de::Error::custom)?,
554            ),
555            "azure_storage_account" => Box::new(
556                serde_json::from_value::<crate::resources::AzureStorageAccountOutputs>(value)
557                    .map_err(serde::de::Error::custom)?,
558            ),
559            "azure_container_apps_environment" => Box::new(
560                serde_json::from_value::<crate::resources::AzureContainerAppsEnvironmentOutputs>(
561                    value,
562                )
563                .map_err(serde::de::Error::custom)?,
564            ),
565            "azure_service_bus_namespace" => Box::new(
566                serde_json::from_value::<crate::resources::AzureServiceBusNamespaceOutputs>(value)
567                    .map_err(serde::de::Error::custom)?,
568            ),
569            other => {
570                return Err(serde::de::Error::unknown_variant(
571                    other,
572                    &[
573                        "vault",
574                        "function",
575                        "container",
576                        "container-cluster",
577                        "storage",
578                        "queue",
579                        "kv",
580                        "network",
581                        "build",
582                        "service-account",
583                        "artifact-registry",
584                        "service_activation",
585                        "remote-stack-management",
586                        "azure_resource_group",
587                        "azure_storage_account",
588                        "azure_container_apps_environment",
589                        "azure_service_bus_namespace",
590                    ],
591                ))
592            }
593        };
594
595        Ok(ResourceOutputs { inner })
596    }
597}
598
599impl ResourceOutputs {
600    /// Creates a new ResourceOutputs from any type that implements ResourceOutputsDefinition
601    pub fn new<T: ResourceOutputsDefinition>(outputs: T) -> Self {
602        Self {
603            inner: Box::new(outputs),
604        }
605    }
606
607    /// Provides access to the underlying ResourceOutputsDefinition trait object
608    pub fn as_resource_outputs(&self) -> &dyn ResourceOutputsDefinition {
609        self.inner.as_ref()
610    }
611
612    /// Generic downcasting for any type
613    pub fn downcast_ref<T: ResourceOutputsDefinition + 'static>(&self) -> Option<&T> {
614        self.inner.as_any().downcast_ref::<T>()
615    }
616}
617
618impl PartialEq for ResourceOutputs {
619    fn eq(&self, other: &Self) -> bool {
620        self.inner.outputs_eq(other.inner.as_ref())
621    }
622}
623
624impl Eq for ResourceOutputs {}
625
626/// OpenAPI schema implementation for ResourceOutputs.
627///
628/// The schema represents the flattened JSON structure of any resource outputs in the Alien system.
629/// All resource outputs have a common base structure with a `type` field, plus type-specific
630/// additional properties that vary depending on the concrete resource implementation.
631///
632/// # Schema Structure
633/// - `type` (required): The resource type identifier (e.g., "function", "storage", "queue")
634/// - Additional properties: Type-specific output fields that vary by resource type
635///
636/// # Example JSON
637/// ```json
638/// {
639///   "type": "function",
640///   "functionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-function",
641///   "functionUrl": "https://abc123.lambda-url.us-east-1.on.aws/"
642/// }
643/// ```
644#[cfg(feature = "openapi")]
645impl PartialSchema for ResourceOutputs {
646    fn schema() -> RefOr<Schema> {
647        RefOr::T(Schema::Object(
648            ObjectBuilder::new()
649                .schema_type(Type::Object)
650                .property("type", Ref::from_schema_name("ResourceType"))
651                .required("type")
652                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
653                .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."))
654                .build()
655        ))
656    }
657}
658
659#[cfg(feature = "openapi")]
660impl ToSchema for ResourceOutputs {
661    fn name() -> std::borrow::Cow<'static, str> {
662        std::borrow::Cow::Borrowed("BaseResourceOutputs")
663    }
664}
665
666/// Represents the high-level status of a resource during its lifecycle.
667#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
668#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
669#[serde(rename_all = "kebab-case")]
670pub enum ResourceStatus {
671    Pending,      // Initial state before any action starts
672    Provisioning, // Resource is being created or updated
673    ProvisionFailed,
674    Running, // Resource is active and configured as desired
675    Updating,
676    UpdateFailed,
677    Deleting, // Resource is being removed
678    DeleteFailed,
679    Deleted,       // Resource has been successfully removed (terminal state)
680    RefreshFailed, // Resource heartbeat/health check failed
681}
682
683impl ResourceStatus {
684    pub fn is_terminal(&self) -> bool {
685        match self {
686            ResourceStatus::Deleted => true,
687            ResourceStatus::ProvisionFailed => true,
688            ResourceStatus::UpdateFailed => true,
689            ResourceStatus::DeleteFailed => true,
690            ResourceStatus::RefreshFailed => true,
691            _ => false, // Pending, Provisioning, Updating, Deleting are not terminal
692        }
693    }
694}
695
696/// Describes the lifecycle of a resource within a stack, determining how it's managed and deployed.
697#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Hash, Deserialize)]
698#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
699#[serde(rename_all = "kebab-case")]
700pub enum ResourceLifecycle {
701    /// Frozen resources are set up once and not modified after creation. They receive
702    /// heartbeat-only permissions for ongoing health checks but no management permissions.
703    /// Example: S3 buckets for logs, VPCs, IAM roles.
704    Frozen,
705
706    /// Live resources are updated on every deploy and require management permissions
707    /// for ongoing updates. All resources (Frozen and Live) are created during initial setup.
708    /// Example: Lambda functions, Cloud Run services.
709    Live,
710}