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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct ResourceType(pub Cow<'static, str>);
17
18impl ResourceType {
19 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 "worker",
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
81pub trait ResourceDefinition: Debug + Send + Sync + 'static {
85 fn get_resource_type(&self) -> ResourceType;
87
88 fn id(&self) -> &str;
90
91 fn get_dependencies(&self) -> Vec<ResourceRef>;
93
94 fn get_permissions(&self) -> Option<&str> {
102 None
103 }
104
105 fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()>;
107
108 fn replace_after_failed_create_is_safe(&self) -> bool {
119 true
120 }
121
122 fn as_any(&self) -> &dyn Any;
124
125 fn as_any_mut(&mut self) -> &mut dyn Any;
127
128 fn box_clone(&self) -> Box<dyn ResourceDefinition>;
130
131 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool;
133
134 fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
136}
137
138impl Clone for Box<dyn ResourceDefinition> {
140 fn clone(&self) -> Self {
141 self.box_clone()
142 }
143}
144
145#[derive(Debug, Clone)]
146pub struct Resource {
147 inner: Box<dyn ResourceDefinition>,
148}
149
150impl Serialize for Resource {
151 fn serialize<S: serde::Serializer>(
152 &self,
153 serializer: S,
154 ) -> std::result::Result<S::Ok, S::Error> {
155 let mut v = self
156 .inner
157 .to_json_value()
158 .map_err(serde::ser::Error::custom)?;
159 v.as_object_mut()
160 .ok_or_else(|| serde::ser::Error::custom("resource must serialize as object"))?
161 .insert(
162 "type".into(),
163 serde_json::Value::String(self.inner.get_resource_type().0.into_owned()),
164 );
165 v.serialize(serializer)
166 }
167}
168
169impl<'de> Deserialize<'de> for Resource {
170 fn deserialize<D: serde::Deserializer<'de>>(
171 deserializer: D,
172 ) -> std::result::Result<Self, D::Error> {
173 let mut value = serde_json::Value::deserialize(deserializer)?;
174 let type_tag = value
175 .get("type")
176 .and_then(|v| v.as_str())
177 .ok_or_else(|| serde::de::Error::missing_field("type"))?
178 .to_string();
179
180 if let Some(obj) = value.as_object_mut() {
183 obj.remove("type");
184 }
185
186 let inner: Box<dyn ResourceDefinition> = match type_tag.as_str() {
187 "vault" => Box::new(
188 serde_json::from_value::<crate::resources::Vault>(value)
189 .map_err(serde::de::Error::custom)?,
190 ),
191 "worker" => Box::new(
192 serde_json::from_value::<crate::resources::Worker>(value)
193 .map_err(serde::de::Error::custom)?,
194 ),
195 "daemon" => Box::new(
196 serde_json::from_value::<crate::resources::Daemon>(value)
197 .map_err(serde::de::Error::custom)?,
198 ),
199 "container" => Box::new(
200 serde_json::from_value::<crate::resources::Container>(value)
201 .map_err(serde::de::Error::custom)?,
202 ),
203 "compute-cluster" => Box::new(
204 serde_json::from_value::<crate::resources::ComputeCluster>(value)
205 .map_err(serde::de::Error::custom)?,
206 ),
207 "kubernetes-cluster" => Box::new(
208 serde_json::from_value::<crate::resources::KubernetesCluster>(value)
209 .map_err(serde::de::Error::custom)?,
210 ),
211 "storage" => Box::new(
212 serde_json::from_value::<crate::resources::Storage>(value)
213 .map_err(serde::de::Error::custom)?,
214 ),
215 "key" => Box::new(
216 serde_json::from_value::<crate::resources::Key>(value)
217 .map_err(serde::de::Error::custom)?,
218 ),
219 "queue" => Box::new(
220 serde_json::from_value::<crate::resources::Queue>(value)
221 .map_err(serde::de::Error::custom)?,
222 ),
223 "sandbox" => Box::new(
224 serde_json::from_value::<crate::resources::Sandbox>(value)
225 .map_err(serde::de::Error::custom)?,
226 ),
227 "email" => Box::new(
228 serde_json::from_value::<crate::resources::Email>(value)
229 .map_err(serde::de::Error::custom)?,
230 ),
231 "kv" => Box::new(
232 serde_json::from_value::<crate::resources::Kv>(value)
233 .map_err(serde::de::Error::custom)?,
234 ),
235 "postgres" => Box::new(
236 serde_json::from_value::<crate::resources::Postgres>(value)
237 .map_err(serde::de::Error::custom)?,
238 ),
239 "redis" => Box::new(
240 serde_json::from_value::<crate::resources::Redis>(value)
241 .map_err(serde::de::Error::custom)?,
242 ),
243 "ai" => Box::new(
244 serde_json::from_value::<crate::resources::Ai>(value)
245 .map_err(serde::de::Error::custom)?,
246 ),
247 "network" => Box::new(
248 serde_json::from_value::<crate::resources::Network>(value)
249 .map_err(serde::de::Error::custom)?,
250 ),
251 "build" => Box::new(
252 serde_json::from_value::<crate::resources::Build>(value)
253 .map_err(serde::de::Error::custom)?,
254 ),
255 "service-account" => Box::new(
256 serde_json::from_value::<crate::resources::ServiceAccount>(value)
257 .map_err(serde::de::Error::custom)?,
258 ),
259 "artifact-registry" => Box::new(
260 serde_json::from_value::<crate::resources::ArtifactRegistry>(value)
261 .map_err(serde::de::Error::custom)?,
262 ),
263 "service_activation" => Box::new(
264 serde_json::from_value::<crate::resources::ServiceActivation>(value)
265 .map_err(serde::de::Error::custom)?,
266 ),
267 "remote-stack-management" => Box::new(
268 serde_json::from_value::<crate::resources::RemoteStackManagement>(value)
269 .map_err(serde::de::Error::custom)?,
270 ),
271 "resource-access" => Box::new(
272 serde_json::from_value::<crate::resources::RemoteBindings>(value)
273 .map_err(serde::de::Error::custom)?,
274 ),
275 "azure_resource_group" => Box::new(
276 serde_json::from_value::<crate::resources::AzureResourceGroup>(value)
277 .map_err(serde::de::Error::custom)?,
278 ),
279 "gcp_agent_platform_engine" => Box::new(
280 serde_json::from_value::<crate::resources::GcpAgentPlatformEngine>(value)
281 .map_err(serde::de::Error::custom)?,
282 ),
283 "azure_storage_account" => Box::new(
284 serde_json::from_value::<crate::resources::AzureStorageAccount>(value)
285 .map_err(serde::de::Error::custom)?,
286 ),
287 "azure_container_apps_environment" => Box::new(
288 serde_json::from_value::<crate::resources::AzureContainerAppsEnvironment>(value)
289 .map_err(serde::de::Error::custom)?,
290 ),
291 "azure_service_bus_namespace" => Box::new(
292 serde_json::from_value::<crate::resources::AzureServiceBusNamespace>(value)
293 .map_err(serde::de::Error::custom)?,
294 ),
295 "experimental/aws-opensearch" => Box::new(
296 serde_json::from_value::<crate::resources::AwsOpenSearch>(value)
297 .map_err(serde::de::Error::custom)?,
298 ),
299 other => {
300 return Err(serde::de::Error::unknown_variant(
301 other,
302 &[
303 "vault",
304 "worker",
305 "daemon",
306 "container",
307 "compute-cluster",
308 "kubernetes-cluster",
309 "storage",
310 "key",
311 "queue",
312 "sandbox",
313 "email",
314 "kv",
315 "postgres",
316 "redis",
317 "ai",
318 "network",
319 "build",
320 "service-account",
321 "artifact-registry",
322 "service_activation",
323 "remote-stack-management",
324 "resource-access",
325 "azure_resource_group",
326 "gcp_agent_platform_engine",
327 "azure_storage_account",
328 "azure_container_apps_environment",
329 "azure_service_bus_namespace",
330 "experimental/aws-opensearch",
331 ],
332 ))
333 }
334 };
335
336 Ok(Resource { inner })
337 }
338}
339
340impl Resource {
341 pub fn new<T: ResourceDefinition>(resource: T) -> Self {
343 Self {
344 inner: Box::new(resource),
345 }
346 }
347
348 pub fn from_boxed(boxed_resource: Box<dyn ResourceDefinition>) -> Self {
350 Self {
351 inner: boxed_resource,
352 }
353 }
354
355 pub fn resource_type(&self) -> ResourceType {
357 self.inner.get_resource_type()
358 }
359
360 pub fn id(&self) -> &str {
362 self.inner.id()
363 }
364
365 pub fn get_dependencies(&self) -> Vec<ResourceRef> {
367 self.inner.get_dependencies()
368 }
369
370 pub fn get_permissions(&self) -> Option<&str> {
372 self.inner.get_permissions()
373 }
374
375 pub fn validate_update(&self, new_config: &Resource) -> Result<()> {
377 self.inner.validate_update(new_config.inner.as_ref())
378 }
379
380 pub fn replace_after_failed_create_is_safe(&self) -> bool {
383 self.inner.replace_after_failed_create_is_safe()
384 }
385
386 pub fn as_resource_definition(&self) -> &dyn ResourceDefinition {
388 self.inner.as_ref()
389 }
390
391 pub fn downcast_ref<T: ResourceDefinition + 'static>(&self) -> Option<&T> {
393 self.inner.as_any().downcast_ref::<T>()
394 }
395
396 pub fn downcast_mut<T: ResourceDefinition + 'static>(&mut self) -> Option<&mut T> {
398 self.inner.as_any_mut().downcast_mut::<T>()
399 }
400}
401
402impl PartialEq for Resource {
403 fn eq(&self, other: &Self) -> bool {
404 self.inner.resource_eq(other.inner.as_ref())
405 }
406}
407
408impl Eq for Resource {}
409
410#[cfg(feature = "openapi")]
432impl PartialSchema for Resource {
433 fn schema() -> RefOr<Schema> {
434 RefOr::T(Schema::Object(
435 ObjectBuilder::new()
436 .schema_type(Type::Object)
437 .property("type", Ref::from_schema_name("ResourceType"))
438 .property("id",
439 ObjectBuilder::new()
440 .schema_type(Type::String)
441 .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."))
442 .build()
443 )
444 .required("type")
445 .required("id")
446 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
447 .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."))
448 .build()
449 ))
450 }
451}
452
453#[cfg(feature = "openapi")]
454impl ToSchema for Resource {
455 fn name() -> std::borrow::Cow<'static, str> {
456 std::borrow::Cow::Borrowed("BaseResource")
457 }
458}
459
460#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
462#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
463#[serde(rename_all = "camelCase")]
464pub struct ResourceRef {
465 #[serde(rename = "type")]
466 pub resource_type: ResourceType,
467 pub id: String,
468}
469
470impl ResourceRef {
471 pub fn new(resource_type: ResourceType, id: impl Into<String>) -> Self {
473 Self {
474 resource_type,
475 id: id.into(),
476 }
477 }
478
479 pub fn resource_type(&self) -> &ResourceType {
481 &self.resource_type
482 }
483
484 pub fn id(&self) -> &str {
486 &self.id
487 }
488}
489
490impl<T: ResourceDefinition> From<&T> for ResourceRef {
491 fn from(resource: &T) -> Self {
492 Self::new(resource.get_resource_type(), resource.id())
493 }
494}
495
496impl From<&Resource> for ResourceRef {
497 fn from(resource: &Resource) -> Self {
498 Self::new(resource.resource_type(), resource.id())
499 }
500}
501
502pub trait ResourceOutputsDefinition: Debug + Send + Sync + 'static {
509 fn is_available(&self) -> bool {
512 true
513 }
514
515 fn get_resource_type(&self) -> ResourceType;
517
518 fn as_any(&self) -> &dyn Any;
520
521 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition>;
523
524 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool;
526
527 fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
529}
530
531impl Clone for Box<dyn ResourceOutputsDefinition> {
533 fn clone(&self) -> Self {
534 self.box_clone()
535 }
536}
537
538#[derive(Debug, Clone)]
541pub struct ResourceOutputs {
542 inner: Box<dyn ResourceOutputsDefinition>,
543}
544
545impl Serialize for ResourceOutputs {
546 fn serialize<S: serde::Serializer>(
547 &self,
548 serializer: S,
549 ) -> std::result::Result<S::Ok, S::Error> {
550 let mut v = self
551 .inner
552 .to_json_value()
553 .map_err(serde::ser::Error::custom)?;
554 v.as_object_mut()
555 .ok_or_else(|| serde::ser::Error::custom("resource outputs must serialize as object"))?
556 .insert(
557 "type".into(),
558 serde_json::Value::String(self.inner.get_resource_type().0.into_owned()),
559 );
560 v.serialize(serializer)
561 }
562}
563
564impl<'de> Deserialize<'de> for ResourceOutputs {
565 fn deserialize<D: serde::Deserializer<'de>>(
566 deserializer: D,
567 ) -> std::result::Result<Self, D::Error> {
568 let mut value = serde_json::Value::deserialize(deserializer)?;
569 let type_tag = value
570 .get("type")
571 .and_then(|v| v.as_str())
572 .ok_or_else(|| serde::de::Error::missing_field("type"))?
573 .to_string();
574
575 if let Some(obj) = value.as_object_mut() {
578 obj.remove("type");
579 }
580
581 let inner: Box<dyn ResourceOutputsDefinition> = match type_tag.as_str() {
582 "vault" => Box::new(
583 serde_json::from_value::<crate::resources::VaultOutputs>(value)
584 .map_err(serde::de::Error::custom)?,
585 ),
586 "worker" => Box::new(
587 serde_json::from_value::<crate::resources::WorkerOutputs>(value)
588 .map_err(serde::de::Error::custom)?,
589 ),
590 "daemon" => Box::new(
591 serde_json::from_value::<crate::resources::DaemonOutputs>(value)
592 .map_err(serde::de::Error::custom)?,
593 ),
594 "container" => Box::new(
595 serde_json::from_value::<crate::resources::ContainerOutputs>(value)
596 .map_err(serde::de::Error::custom)?,
597 ),
598 "compute-cluster" => Box::new(
599 serde_json::from_value::<crate::resources::ComputeClusterOutputs>(value)
600 .map_err(serde::de::Error::custom)?,
601 ),
602 "storage" => Box::new(
603 serde_json::from_value::<crate::resources::StorageOutputs>(value)
604 .map_err(serde::de::Error::custom)?,
605 ),
606 "key" => Box::new(
607 serde_json::from_value::<crate::resources::KeyOutputs>(value)
608 .map_err(serde::de::Error::custom)?,
609 ),
610 "queue" => Box::new(
611 serde_json::from_value::<crate::resources::QueueOutputs>(value)
612 .map_err(serde::de::Error::custom)?,
613 ),
614 "sandbox" => Box::new(
615 serde_json::from_value::<crate::resources::SandboxOutputs>(value)
616 .map_err(serde::de::Error::custom)?,
617 ),
618 "email" => Box::new(
619 serde_json::from_value::<crate::resources::EmailOutputs>(value)
620 .map_err(serde::de::Error::custom)?,
621 ),
622 "kv" => Box::new(
623 serde_json::from_value::<crate::resources::KvOutputs>(value)
624 .map_err(serde::de::Error::custom)?,
625 ),
626 "postgres" => Box::new(
627 serde_json::from_value::<crate::resources::PostgresOutputs>(value)
628 .map_err(serde::de::Error::custom)?,
629 ),
630 "redis" => Box::new(
631 serde_json::from_value::<crate::resources::RedisOutputs>(value)
632 .map_err(serde::de::Error::custom)?,
633 ),
634 "ai" => Box::new(
635 serde_json::from_value::<crate::resources::AiOutputs>(value)
636 .map_err(serde::de::Error::custom)?,
637 ),
638 "network" => Box::new(
639 serde_json::from_value::<crate::resources::NetworkOutputs>(value)
640 .map_err(serde::de::Error::custom)?,
641 ),
642 "build" => Box::new(
643 serde_json::from_value::<crate::resources::BuildOutputs>(value)
644 .map_err(serde::de::Error::custom)?,
645 ),
646 "service-account" => Box::new(
647 serde_json::from_value::<crate::resources::ServiceAccountOutputs>(value)
648 .map_err(serde::de::Error::custom)?,
649 ),
650 "artifact-registry" => Box::new(
651 serde_json::from_value::<crate::resources::ArtifactRegistryOutputs>(value)
652 .map_err(serde::de::Error::custom)?,
653 ),
654 "service_activation" => Box::new(
655 serde_json::from_value::<crate::resources::ServiceActivationOutputs>(value)
656 .map_err(serde::de::Error::custom)?,
657 ),
658 "remote-stack-management" => Box::new(
659 serde_json::from_value::<crate::resources::RemoteStackManagementOutputs>(value)
660 .map_err(serde::de::Error::custom)?,
661 ),
662 "resource-access" => Box::new(
663 serde_json::from_value::<crate::resources::RemoteBindingsOutputs>(value)
664 .map_err(serde::de::Error::custom)?,
665 ),
666 "kubernetes-cluster" => Box::new(
667 serde_json::from_value::<crate::resources::KubernetesClusterOutputs>(value)
668 .map_err(serde::de::Error::custom)?,
669 ),
670 "azure_resource_group" => Box::new(
671 serde_json::from_value::<crate::resources::AzureResourceGroupOutputs>(value)
672 .map_err(serde::de::Error::custom)?,
673 ),
674 "azure_storage_account" => Box::new(
675 serde_json::from_value::<crate::resources::AzureStorageAccountOutputs>(value)
676 .map_err(serde::de::Error::custom)?,
677 ),
678 "azure_container_apps_environment" => Box::new(
679 serde_json::from_value::<crate::resources::AzureContainerAppsEnvironmentOutputs>(
680 value,
681 )
682 .map_err(serde::de::Error::custom)?,
683 ),
684 "azure_service_bus_namespace" => Box::new(
685 serde_json::from_value::<crate::resources::AzureServiceBusNamespaceOutputs>(value)
686 .map_err(serde::de::Error::custom)?,
687 ),
688 "experimental/aws-opensearch" => Box::new(
689 serde_json::from_value::<crate::resources::AwsOpenSearchOutputs>(value)
690 .map_err(serde::de::Error::custom)?,
691 ),
692 other => {
693 return Err(serde::de::Error::unknown_variant(
694 other,
695 &[
696 "vault",
697 "worker",
698 "daemon",
699 "container",
700 "compute-cluster",
701 "storage",
702 "key",
703 "queue",
704 "sandbox",
705 "email",
706 "kv",
707 "postgres",
708 "redis",
709 "ai",
710 "network",
711 "build",
712 "service-account",
713 "artifact-registry",
714 "service_activation",
715 "remote-stack-management",
716 "resource-access",
717 "kubernetes-cluster",
718 "azure_resource_group",
719 "azure_storage_account",
720 "azure_container_apps_environment",
721 "azure_service_bus_namespace",
722 "experimental/aws-opensearch",
723 ],
724 ))
725 }
726 };
727
728 Ok(ResourceOutputs { inner })
729 }
730}
731
732impl ResourceOutputs {
733 pub fn new<T: ResourceOutputsDefinition>(outputs: T) -> Self {
735 Self {
736 inner: Box::new(outputs),
737 }
738 }
739
740 pub fn as_resource_outputs(&self) -> &dyn ResourceOutputsDefinition {
742 self.inner.as_ref()
743 }
744
745 pub fn downcast_ref<T: ResourceOutputsDefinition + 'static>(&self) -> Option<&T> {
747 self.inner.as_any().downcast_ref::<T>()
748 }
749}
750
751impl PartialEq for ResourceOutputs {
752 fn eq(&self, other: &Self) -> bool {
753 self.inner.outputs_eq(other.inner.as_ref())
754 }
755}
756
757impl Eq for ResourceOutputs {}
758
759#[cfg(feature = "openapi")]
778impl PartialSchema for ResourceOutputs {
779 fn schema() -> RefOr<Schema> {
780 RefOr::T(Schema::Object(
781 ObjectBuilder::new()
782 .schema_type(Type::Object)
783 .property("type", Ref::from_schema_name("ResourceType"))
784 .required("type")
785 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
786 .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."))
787 .build()
788 ))
789 }
790}
791
792#[cfg(feature = "openapi")]
793impl ToSchema for ResourceOutputs {
794 fn name() -> std::borrow::Cow<'static, str> {
795 std::borrow::Cow::Borrowed("BaseResourceOutputs")
796 }
797}
798
799#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
801#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
802#[serde(rename_all = "kebab-case")]
803pub enum ResourceStatus {
804 Pending, Provisioning, ProvisionFailed,
807 Running, Updating,
809 UpdateFailed,
810 Deleting, DeleteFailed,
812 TeardownRequired, Deleted, RefreshFailed, }
816
817impl ResourceStatus {
818 pub fn is_terminal(&self) -> bool {
819 match self {
820 ResourceStatus::TeardownRequired => true,
821 ResourceStatus::Deleted => true,
822 ResourceStatus::ProvisionFailed => true,
823 ResourceStatus::UpdateFailed => true,
824 ResourceStatus::DeleteFailed => true,
825 ResourceStatus::RefreshFailed => true,
826 _ => false, }
828 }
829}
830
831#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Hash, Deserialize)]
833#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
834#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
835#[serde(rename_all = "kebab-case")]
836pub enum ResourceLifecycle {
837 Frozen,
841
842 Live,
846}