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>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
138        let mut v = self.inner.to_json_value().map_err(serde::ser::Error::custom)?;
139        v.as_object_mut()
140            .ok_or_else(|| serde::ser::Error::custom("resource must serialize as object"))?
141            .insert("type".into(), serde_json::Value::String(self.inner.get_resource_type().0.into_owned()));
142        v.serialize(serializer)
143    }
144}
145
146impl<'de> Deserialize<'de> for Resource {
147    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
148        let mut value = serde_json::Value::deserialize(deserializer)?;
149        let type_tag = value.get("type").and_then(|v| v.as_str())
150            .ok_or_else(|| serde::de::Error::missing_field("type"))?
151            .to_string();
152
153        // Remove the "type" tag before passing to concrete deserializer
154        // (structs with deny_unknown_fields would reject it)
155        if let Some(obj) = value.as_object_mut() {
156            obj.remove("type");
157        }
158
159        let inner: Box<dyn ResourceDefinition> = match type_tag.as_str() {
160            "vault" => Box::new(serde_json::from_value::<crate::resources::Vault>(value).map_err(serde::de::Error::custom)?),
161            "function" => Box::new(serde_json::from_value::<crate::resources::Function>(value).map_err(serde::de::Error::custom)?),
162            "container" => Box::new(serde_json::from_value::<crate::resources::Container>(value).map_err(serde::de::Error::custom)?),
163            "container-cluster" => Box::new(serde_json::from_value::<crate::resources::ContainerCluster>(value).map_err(serde::de::Error::custom)?),
164            "storage" => Box::new(serde_json::from_value::<crate::resources::Storage>(value).map_err(serde::de::Error::custom)?),
165            "queue" => Box::new(serde_json::from_value::<crate::resources::Queue>(value).map_err(serde::de::Error::custom)?),
166            "kv" => Box::new(serde_json::from_value::<crate::resources::Kv>(value).map_err(serde::de::Error::custom)?),
167            "network" => Box::new(serde_json::from_value::<crate::resources::Network>(value).map_err(serde::de::Error::custom)?),
168            "build" => Box::new(serde_json::from_value::<crate::resources::Build>(value).map_err(serde::de::Error::custom)?),
169            "service-account" => Box::new(serde_json::from_value::<crate::resources::ServiceAccount>(value).map_err(serde::de::Error::custom)?),
170            "artifact-registry" => Box::new(serde_json::from_value::<crate::resources::ArtifactRegistry>(value).map_err(serde::de::Error::custom)?),
171            "service_activation" => Box::new(serde_json::from_value::<crate::resources::ServiceActivation>(value).map_err(serde::de::Error::custom)?),
172            "remote-stack-management" => Box::new(serde_json::from_value::<crate::resources::RemoteStackManagement>(value).map_err(serde::de::Error::custom)?),
173            "azure_resource_group" => Box::new(serde_json::from_value::<crate::resources::AzureResourceGroup>(value).map_err(serde::de::Error::custom)?),
174            "azure_storage_account" => Box::new(serde_json::from_value::<crate::resources::AzureStorageAccount>(value).map_err(serde::de::Error::custom)?),
175            "azure_container_apps_environment" => Box::new(serde_json::from_value::<crate::resources::AzureContainerAppsEnvironment>(value).map_err(serde::de::Error::custom)?),
176            "azure_service_bus_namespace" => Box::new(serde_json::from_value::<crate::resources::AzureServiceBusNamespace>(value).map_err(serde::de::Error::custom)?),
177            other => return Err(serde::de::Error::unknown_variant(other, &[
178                "vault", "function", "container", "container-cluster", "storage", "queue", "kv",
179                "network", "build", "service-account", "artifact-registry", "service_activation",
180                "remote-stack-management", "azure_resource_group", "azure_storage_account",
181                "azure_container_apps_environment", "azure_service_bus_namespace",
182            ])),
183        };
184
185        Ok(Resource { inner })
186    }
187}
188
189impl Resource {
190    /// Creates a new Resource from any type that implements ResourceDefinition
191    pub fn new<T: ResourceDefinition>(resource: T) -> Self {
192        Self {
193            inner: Box::new(resource),
194        }
195    }
196
197    /// Creates a new Resource from a boxed ResourceDefinition
198    pub fn from_boxed(boxed_resource: Box<dyn ResourceDefinition>) -> Self {
199        Self {
200            inner: boxed_resource,
201        }
202    }
203
204    /// Returns the resource type identifier
205    pub fn resource_type(&self) -> ResourceType {
206        self.inner.get_resource_type()
207    }
208
209    /// Returns the unique identifier for this resource instance
210    pub fn id(&self) -> &str {
211        self.inner.id()
212    }
213
214    /// Returns the list of other resources this resource depends on
215    pub fn get_dependencies(&self) -> Vec<ResourceRef> {
216        self.inner.get_dependencies()
217    }
218
219    /// Returns the permission profile name for this resource, if it has one.
220    pub fn get_permissions(&self) -> Option<&str> {
221        self.inner.get_permissions()
222    }
223
224    /// Validates if an update from the current configuration to a new configuration is allowed
225    pub fn validate_update(&self, new_config: &Resource) -> Result<()> {
226        self.inner.validate_update(new_config.inner.as_ref())
227    }
228
229    /// Provides access to the underlying ResourceDefinition trait object
230    pub fn as_resource_definition(&self) -> &dyn ResourceDefinition {
231        self.inner.as_ref()
232    }
233
234    /// Generic downcasting for any type
235    pub fn downcast_ref<T: ResourceDefinition + 'static>(&self) -> Option<&T> {
236        self.inner.as_any().downcast_ref::<T>()
237    }
238
239    /// Generic mutable downcasting for any type
240    pub fn downcast_mut<T: ResourceDefinition + 'static>(&mut self) -> Option<&mut T> {
241        self.inner.as_any_mut().downcast_mut::<T>()
242    }
243}
244
245impl PartialEq for Resource {
246    fn eq(&self, other: &Self) -> bool {
247        self.inner.resource_eq(other.inner.as_ref())
248    }
249}
250
251impl Eq for Resource {}
252
253/// OpenAPI schema implementation for Resource.
254///
255/// The schema represents the flattened JSON structure of any resource type in the Alien system.
256/// All resources have a common base structure with `type` and `id` fields, plus type-specific
257/// additional properties that vary depending on the concrete resource implementation.
258///
259/// # Schema Structure
260/// - `type` (required): The resource type identifier (e.g., "function", "storage", "queue")
261/// - `id` (required): The unique identifier for this specific resource instance
262/// - Additional properties: Type-specific fields that vary by resource type (e.g., Function has `code`, `memory_mb`, etc.)
263///
264/// # Example JSON
265/// ```json
266/// {
267///   "type": "function",
268///   "id": "my-function",
269///   "code": { "type": "image", "image": "my-image:latest" },
270///   "memoryMb": 512,
271///   "timeoutSeconds": 30
272/// }
273/// ```
274#[cfg(feature = "openapi")]
275impl PartialSchema for Resource {
276    fn schema() -> RefOr<Schema> {
277        RefOr::T(Schema::Object(
278            ObjectBuilder::new()
279                .schema_type(Type::Object)
280                .property("type", Ref::from_schema_name("ResourceType"))
281                .property("id",
282                    ObjectBuilder::new()
283                        .schema_type(Type::String)
284                        .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."))
285                        .build()
286                )
287                .required("type")
288                .required("id")
289                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
290                .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."))
291                .build()
292        ))
293    }
294}
295
296#[cfg(feature = "openapi")]
297impl ToSchema for Resource {
298    fn name() -> std::borrow::Cow<'static, str> {
299        std::borrow::Cow::Borrowed("BaseResource")
300    }
301}
302
303/// New ResourceRef that works with any resource type.
304/// This can eventually replace the enum-based ResourceRef for full extensibility.
305#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
306#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
307#[serde(rename_all = "camelCase")]
308pub struct ResourceRef {
309    #[serde(rename = "type")]
310    pub resource_type: ResourceType,
311    pub id: String,
312}
313
314impl ResourceRef {
315    /// Creates a new ResourceRef
316    pub fn new(resource_type: ResourceType, id: impl Into<String>) -> Self {
317        Self {
318            resource_type,
319            id: id.into(),
320        }
321    }
322
323    /// Returns the resource type
324    pub fn resource_type(&self) -> &ResourceType {
325        &self.resource_type
326    }
327
328    /// Returns the resource id
329    pub fn id(&self) -> &str {
330        &self.id
331    }
332}
333
334impl<T: ResourceDefinition> From<&T> for ResourceRef {
335    fn from(resource: &T) -> Self {
336        Self::new(resource.get_resource_type(), resource.id())
337    }
338}
339
340impl From<&Resource> for ResourceRef {
341    fn from(resource: &Resource) -> Self {
342        Self::new(resource.resource_type(), resource.id())
343    }
344}
345
346/// Trait that defines the interface for all resource output types in the Alien system.
347/// This trait enables extensibility by allowing new resource output types to be registered
348/// and managed alongside built-in resource outputs.
349pub trait ResourceOutputsDefinition: Debug + Send + Sync + 'static {
350    /// Returns the resource type for this instance
351    fn get_resource_type(&self) -> ResourceType;
352
353    /// Provides access to the underlying concrete type for downcasting
354    fn as_any(&self) -> &dyn Any;
355
356    /// Creates a boxed clone of this resource outputs
357    fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition>;
358
359    /// For equality comparison between resource outputs
360    fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool;
361
362    /// Serialize this resource outputs to a JSON value (without the "type" tag - that's added by ResourceOutputs)
363    fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
364}
365
366/// Clone implementation for boxed ResourceOutputsDefinition trait objects
367impl Clone for Box<dyn ResourceOutputsDefinition> {
368    fn clone(&self) -> Self {
369        self.box_clone()
370    }
371}
372
373/// New Resource outputs wrapper that can hold any ResourceOutputsDefinition.
374/// This replaces the old ResourceOutputs enum to enable runtime extensibility.
375#[derive(Debug, Clone)]
376pub struct ResourceOutputs {
377    inner: Box<dyn ResourceOutputsDefinition>,
378}
379
380impl Serialize for ResourceOutputs {
381    fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
382        let mut v = self.inner.to_json_value().map_err(serde::ser::Error::custom)?;
383        v.as_object_mut()
384            .ok_or_else(|| serde::ser::Error::custom("resource outputs must serialize as object"))?
385            .insert("type".into(), serde_json::Value::String(self.inner.get_resource_type().0.into_owned()));
386        v.serialize(serializer)
387    }
388}
389
390impl<'de> Deserialize<'de> for ResourceOutputs {
391    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
392        let mut value = serde_json::Value::deserialize(deserializer)?;
393        let type_tag = value.get("type").and_then(|v| v.as_str())
394            .ok_or_else(|| serde::de::Error::missing_field("type"))?
395            .to_string();
396
397        // Remove the "type" tag before passing to concrete deserializer
398        // (structs with deny_unknown_fields would reject it)
399        if let Some(obj) = value.as_object_mut() {
400            obj.remove("type");
401        }
402
403        let inner: Box<dyn ResourceOutputsDefinition> = match type_tag.as_str() {
404            "vault" => Box::new(serde_json::from_value::<crate::resources::VaultOutputs>(value).map_err(serde::de::Error::custom)?),
405            "function" => Box::new(serde_json::from_value::<crate::resources::FunctionOutputs>(value).map_err(serde::de::Error::custom)?),
406            "container" => Box::new(serde_json::from_value::<crate::resources::ContainerOutputs>(value).map_err(serde::de::Error::custom)?),
407            "container-cluster" => Box::new(serde_json::from_value::<crate::resources::ContainerClusterOutputs>(value).map_err(serde::de::Error::custom)?),
408            "storage" => Box::new(serde_json::from_value::<crate::resources::StorageOutputs>(value).map_err(serde::de::Error::custom)?),
409            "queue" => Box::new(serde_json::from_value::<crate::resources::QueueOutputs>(value).map_err(serde::de::Error::custom)?),
410            "kv" => Box::new(serde_json::from_value::<crate::resources::KvOutputs>(value).map_err(serde::de::Error::custom)?),
411            "network" => Box::new(serde_json::from_value::<crate::resources::NetworkOutputs>(value).map_err(serde::de::Error::custom)?),
412            "build" => Box::new(serde_json::from_value::<crate::resources::BuildOutputs>(value).map_err(serde::de::Error::custom)?),
413            "service-account" => Box::new(serde_json::from_value::<crate::resources::ServiceAccountOutputs>(value).map_err(serde::de::Error::custom)?),
414            "artifact-registry" => Box::new(serde_json::from_value::<crate::resources::ArtifactRegistryOutputs>(value).map_err(serde::de::Error::custom)?),
415            "service_activation" => Box::new(serde_json::from_value::<crate::resources::ServiceActivationOutputs>(value).map_err(serde::de::Error::custom)?),
416            "remote-stack-management" => Box::new(serde_json::from_value::<crate::resources::RemoteStackManagementOutputs>(value).map_err(serde::de::Error::custom)?),
417            "azure_resource_group" => Box::new(serde_json::from_value::<crate::resources::AzureResourceGroupOutputs>(value).map_err(serde::de::Error::custom)?),
418            "azure_storage_account" => Box::new(serde_json::from_value::<crate::resources::AzureStorageAccountOutputs>(value).map_err(serde::de::Error::custom)?),
419            "azure_container_apps_environment" => Box::new(serde_json::from_value::<crate::resources::AzureContainerAppsEnvironmentOutputs>(value).map_err(serde::de::Error::custom)?),
420            "azure_service_bus_namespace" => Box::new(serde_json::from_value::<crate::resources::AzureServiceBusNamespaceOutputs>(value).map_err(serde::de::Error::custom)?),
421            other => return Err(serde::de::Error::unknown_variant(other, &[
422                "vault", "function", "container", "container-cluster", "storage", "queue", "kv",
423                "network", "build", "service-account", "artifact-registry", "service_activation",
424                "remote-stack-management", "azure_resource_group", "azure_storage_account",
425                "azure_container_apps_environment", "azure_service_bus_namespace",
426            ])),
427        };
428
429        Ok(ResourceOutputs { inner })
430    }
431}
432
433impl ResourceOutputs {
434    /// Creates a new ResourceOutputs from any type that implements ResourceOutputsDefinition
435    pub fn new<T: ResourceOutputsDefinition>(outputs: T) -> Self {
436        Self {
437            inner: Box::new(outputs),
438        }
439    }
440
441    /// Provides access to the underlying ResourceOutputsDefinition trait object
442    pub fn as_resource_outputs(&self) -> &dyn ResourceOutputsDefinition {
443        self.inner.as_ref()
444    }
445
446    /// Generic downcasting for any type
447    pub fn downcast_ref<T: ResourceOutputsDefinition + 'static>(&self) -> Option<&T> {
448        self.inner.as_any().downcast_ref::<T>()
449    }
450}
451
452impl PartialEq for ResourceOutputs {
453    fn eq(&self, other: &Self) -> bool {
454        self.inner.outputs_eq(other.inner.as_ref())
455    }
456}
457
458impl Eq for ResourceOutputs {}
459
460/// OpenAPI schema implementation for ResourceOutputs.
461///
462/// The schema represents the flattened JSON structure of any resource outputs in the Alien system.
463/// All resource outputs have a common base structure with a `type` field, plus type-specific
464/// additional properties that vary depending on the concrete resource implementation.
465///
466/// # Schema Structure
467/// - `type` (required): The resource type identifier (e.g., "function", "storage", "queue")
468/// - Additional properties: Type-specific output fields that vary by resource type
469///
470/// # Example JSON
471/// ```json
472/// {
473///   "type": "function",
474///   "functionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-function",
475///   "functionUrl": "https://abc123.lambda-url.us-east-1.on.aws/"
476/// }
477/// ```
478#[cfg(feature = "openapi")]
479impl PartialSchema for ResourceOutputs {
480    fn schema() -> RefOr<Schema> {
481        RefOr::T(Schema::Object(
482            ObjectBuilder::new()
483                .schema_type(Type::Object)
484                .property("type", Ref::from_schema_name("ResourceType"))
485                .required("type")
486                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
487                .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."))
488                .build()
489        ))
490    }
491}
492
493#[cfg(feature = "openapi")]
494impl ToSchema for ResourceOutputs {
495    fn name() -> std::borrow::Cow<'static, str> {
496        std::borrow::Cow::Borrowed("BaseResourceOutputs")
497    }
498}
499
500/// Represents the high-level status of a resource during its lifecycle.
501#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
502#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
503#[serde(rename_all = "kebab-case")]
504pub enum ResourceStatus {
505    Pending,      // Initial state before any action starts
506    Provisioning, // Resource is being created or updated
507    ProvisionFailed,
508    Running, // Resource is active and configured as desired
509    Updating,
510    UpdateFailed,
511    Deleting, // Resource is being removed
512    DeleteFailed,
513    Deleted,       // Resource has been successfully removed (terminal state)
514    RefreshFailed, // Resource heartbeat/health check failed
515}
516
517impl ResourceStatus {
518    pub fn is_terminal(&self) -> bool {
519        match self {
520            ResourceStatus::Deleted => true,
521            ResourceStatus::ProvisionFailed => true,
522            ResourceStatus::UpdateFailed => true,
523            ResourceStatus::DeleteFailed => true,
524            ResourceStatus::RefreshFailed => true,
525            _ => false, // Pending, Provisioning, Updating, Deleting are not terminal
526        }
527    }
528}
529
530/// Describes the lifecycle of a resource within a stack, determining how it's managed and deployed.
531#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Hash, Deserialize)]
532#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
533#[serde(rename_all = "kebab-case")]
534pub enum ResourceLifecycle {
535    /// Frozen resources are created once during initial setup and are rarely, if ever, modified.
536    /// They typically require fewer permissions for ongoing management after the initial deployment.
537    /// Example: S3 buckets for logs, VPCs, IAM roles.
538    Frozen,
539
540    /// Live resources are frequently updated as part of ongoing deployments.
541    /// They generally require more permissions for ongoing management to allow for these frequent updates.
542    /// By default, live resources are not created during the initial setup phase unless specified.
543    /// Example: Lambda functions, Cloud Run services.
544    Live,
545
546    /// LiveOnSetup resources are live resources that are specifically designated to be created
547    /// during the initial setup phase of the stack. This is useful for resources that need to be
548    /// present from the beginning but are still expected to be updated frequently.
549    /// Example: A managing function that orchestrates updates for other live resources.
550    LiveOnSetup,
551}
552
553impl ResourceLifecycle {
554    /// Returns `true` if the resource is considered live (i.e., `Live` or `LiveOnSetup`).
555    pub fn is_live(&self) -> bool {
556        match self {
557            ResourceLifecycle::Frozen => false,
558            ResourceLifecycle::Live | ResourceLifecycle::LiveOnSetup => true,
559        }
560    }
561
562    /// Returns `true` if the resource should be created or configured during the initial setup phase.
563    /// This applies to `Frozen` resources and `LiveOnSetup` resources.
564    pub fn initial_setup(&self) -> bool {
565        match self {
566            ResourceLifecycle::Frozen | ResourceLifecycle::LiveOnSetup => true,
567            ResourceLifecycle::Live => false,
568        }
569    }
570}