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 as_any(&self) -> &dyn Any;
110
111 fn as_any_mut(&mut self) -> &mut dyn Any;
113
114 fn box_clone(&self) -> Box<dyn ResourceDefinition>;
116
117 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool;
119
120 fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
122}
123
124impl 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 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 "worker" => Box::new(
178 serde_json::from_value::<crate::resources::Worker>(value)
179 .map_err(serde::de::Error::custom)?,
180 ),
181 "daemon" => Box::new(
182 serde_json::from_value::<crate::resources::Daemon>(value)
183 .map_err(serde::de::Error::custom)?,
184 ),
185 "container" => Box::new(
186 serde_json::from_value::<crate::resources::Container>(value)
187 .map_err(serde::de::Error::custom)?,
188 ),
189 "compute-cluster" => Box::new(
190 serde_json::from_value::<crate::resources::ComputeCluster>(value)
191 .map_err(serde::de::Error::custom)?,
192 ),
193 "kubernetes-cluster" => Box::new(
194 serde_json::from_value::<crate::resources::KubernetesCluster>(value)
195 .map_err(serde::de::Error::custom)?,
196 ),
197 "storage" => Box::new(
198 serde_json::from_value::<crate::resources::Storage>(value)
199 .map_err(serde::de::Error::custom)?,
200 ),
201 "queue" => Box::new(
202 serde_json::from_value::<crate::resources::Queue>(value)
203 .map_err(serde::de::Error::custom)?,
204 ),
205 "kv" => Box::new(
206 serde_json::from_value::<crate::resources::Kv>(value)
207 .map_err(serde::de::Error::custom)?,
208 ),
209 "postgres" => Box::new(
210 serde_json::from_value::<crate::resources::Postgres>(value)
211 .map_err(serde::de::Error::custom)?,
212 ),
213 "network" => Box::new(
214 serde_json::from_value::<crate::resources::Network>(value)
215 .map_err(serde::de::Error::custom)?,
216 ),
217 "build" => Box::new(
218 serde_json::from_value::<crate::resources::Build>(value)
219 .map_err(serde::de::Error::custom)?,
220 ),
221 "service-account" => Box::new(
222 serde_json::from_value::<crate::resources::ServiceAccount>(value)
223 .map_err(serde::de::Error::custom)?,
224 ),
225 "artifact-registry" => Box::new(
226 serde_json::from_value::<crate::resources::ArtifactRegistry>(value)
227 .map_err(serde::de::Error::custom)?,
228 ),
229 "service_activation" => Box::new(
230 serde_json::from_value::<crate::resources::ServiceActivation>(value)
231 .map_err(serde::de::Error::custom)?,
232 ),
233 "remote-stack-management" => Box::new(
234 serde_json::from_value::<crate::resources::RemoteStackManagement>(value)
235 .map_err(serde::de::Error::custom)?,
236 ),
237 "azure_resource_group" => Box::new(
238 serde_json::from_value::<crate::resources::AzureResourceGroup>(value)
239 .map_err(serde::de::Error::custom)?,
240 ),
241 "azure_storage_account" => Box::new(
242 serde_json::from_value::<crate::resources::AzureStorageAccount>(value)
243 .map_err(serde::de::Error::custom)?,
244 ),
245 "azure_container_apps_environment" => Box::new(
246 serde_json::from_value::<crate::resources::AzureContainerAppsEnvironment>(value)
247 .map_err(serde::de::Error::custom)?,
248 ),
249 "azure_service_bus_namespace" => Box::new(
250 serde_json::from_value::<crate::resources::AzureServiceBusNamespace>(value)
251 .map_err(serde::de::Error::custom)?,
252 ),
253 other => {
254 return Err(serde::de::Error::unknown_variant(
255 other,
256 &[
257 "vault",
258 "worker",
259 "daemon",
260 "container",
261 "compute-cluster",
262 "kubernetes-cluster",
263 "storage",
264 "queue",
265 "kv",
266 "postgres",
267 "network",
268 "build",
269 "service-account",
270 "artifact-registry",
271 "service_activation",
272 "remote-stack-management",
273 "azure_resource_group",
274 "azure_storage_account",
275 "azure_container_apps_environment",
276 "azure_service_bus_namespace",
277 ],
278 ))
279 }
280 };
281
282 Ok(Resource { inner })
283 }
284}
285
286impl Resource {
287 pub fn new<T: ResourceDefinition>(resource: T) -> Self {
289 Self {
290 inner: Box::new(resource),
291 }
292 }
293
294 pub fn from_boxed(boxed_resource: Box<dyn ResourceDefinition>) -> Self {
296 Self {
297 inner: boxed_resource,
298 }
299 }
300
301 pub fn resource_type(&self) -> ResourceType {
303 self.inner.get_resource_type()
304 }
305
306 pub fn id(&self) -> &str {
308 self.inner.id()
309 }
310
311 pub fn get_dependencies(&self) -> Vec<ResourceRef> {
313 self.inner.get_dependencies()
314 }
315
316 pub fn get_permissions(&self) -> Option<&str> {
318 self.inner.get_permissions()
319 }
320
321 pub fn validate_update(&self, new_config: &Resource) -> Result<()> {
323 self.inner.validate_update(new_config.inner.as_ref())
324 }
325
326 pub fn as_resource_definition(&self) -> &dyn ResourceDefinition {
328 self.inner.as_ref()
329 }
330
331 pub fn downcast_ref<T: ResourceDefinition + 'static>(&self) -> Option<&T> {
333 self.inner.as_any().downcast_ref::<T>()
334 }
335
336 pub fn downcast_mut<T: ResourceDefinition + 'static>(&mut self) -> Option<&mut T> {
338 self.inner.as_any_mut().downcast_mut::<T>()
339 }
340}
341
342impl PartialEq for Resource {
343 fn eq(&self, other: &Self) -> bool {
344 self.inner.resource_eq(other.inner.as_ref())
345 }
346}
347
348impl Eq for Resource {}
349
350#[cfg(feature = "openapi")]
372impl PartialSchema for Resource {
373 fn schema() -> RefOr<Schema> {
374 RefOr::T(Schema::Object(
375 ObjectBuilder::new()
376 .schema_type(Type::Object)
377 .property("type", Ref::from_schema_name("ResourceType"))
378 .property("id",
379 ObjectBuilder::new()
380 .schema_type(Type::String)
381 .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."))
382 .build()
383 )
384 .required("type")
385 .required("id")
386 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
387 .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."))
388 .build()
389 ))
390 }
391}
392
393#[cfg(feature = "openapi")]
394impl ToSchema for Resource {
395 fn name() -> std::borrow::Cow<'static, str> {
396 std::borrow::Cow::Borrowed("BaseResource")
397 }
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
403#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
404#[serde(rename_all = "camelCase")]
405pub struct ResourceRef {
406 #[serde(rename = "type")]
407 pub resource_type: ResourceType,
408 pub id: String,
409}
410
411impl ResourceRef {
412 pub fn new(resource_type: ResourceType, id: impl Into<String>) -> Self {
414 Self {
415 resource_type,
416 id: id.into(),
417 }
418 }
419
420 pub fn resource_type(&self) -> &ResourceType {
422 &self.resource_type
423 }
424
425 pub fn id(&self) -> &str {
427 &self.id
428 }
429}
430
431impl<T: ResourceDefinition> From<&T> for ResourceRef {
432 fn from(resource: &T) -> Self {
433 Self::new(resource.get_resource_type(), resource.id())
434 }
435}
436
437impl From<&Resource> for ResourceRef {
438 fn from(resource: &Resource) -> Self {
439 Self::new(resource.resource_type(), resource.id())
440 }
441}
442
443pub trait ResourceOutputsDefinition: Debug + Send + Sync + 'static {
447 fn get_resource_type(&self) -> ResourceType;
449
450 fn as_any(&self) -> &dyn Any;
452
453 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition>;
455
456 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool;
458
459 fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
461}
462
463impl Clone for Box<dyn ResourceOutputsDefinition> {
465 fn clone(&self) -> Self {
466 self.box_clone()
467 }
468}
469
470#[derive(Debug, Clone)]
473pub struct ResourceOutputs {
474 inner: Box<dyn ResourceOutputsDefinition>,
475}
476
477impl Serialize for ResourceOutputs {
478 fn serialize<S: serde::Serializer>(
479 &self,
480 serializer: S,
481 ) -> std::result::Result<S::Ok, S::Error> {
482 let mut v = self
483 .inner
484 .to_json_value()
485 .map_err(serde::ser::Error::custom)?;
486 v.as_object_mut()
487 .ok_or_else(|| serde::ser::Error::custom("resource outputs must serialize as object"))?
488 .insert(
489 "type".into(),
490 serde_json::Value::String(self.inner.get_resource_type().0.into_owned()),
491 );
492 v.serialize(serializer)
493 }
494}
495
496impl<'de> Deserialize<'de> for ResourceOutputs {
497 fn deserialize<D: serde::Deserializer<'de>>(
498 deserializer: D,
499 ) -> std::result::Result<Self, D::Error> {
500 let mut value = serde_json::Value::deserialize(deserializer)?;
501 let type_tag = value
502 .get("type")
503 .and_then(|v| v.as_str())
504 .ok_or_else(|| serde::de::Error::missing_field("type"))?
505 .to_string();
506
507 if let Some(obj) = value.as_object_mut() {
510 obj.remove("type");
511 }
512
513 let inner: Box<dyn ResourceOutputsDefinition> = match type_tag.as_str() {
514 "vault" => Box::new(
515 serde_json::from_value::<crate::resources::VaultOutputs>(value)
516 .map_err(serde::de::Error::custom)?,
517 ),
518 "worker" => Box::new(
519 serde_json::from_value::<crate::resources::WorkerOutputs>(value)
520 .map_err(serde::de::Error::custom)?,
521 ),
522 "daemon" => Box::new(
523 serde_json::from_value::<crate::resources::DaemonOutputs>(value)
524 .map_err(serde::de::Error::custom)?,
525 ),
526 "container" => Box::new(
527 serde_json::from_value::<crate::resources::ContainerOutputs>(value)
528 .map_err(serde::de::Error::custom)?,
529 ),
530 "compute-cluster" => Box::new(
531 serde_json::from_value::<crate::resources::ComputeClusterOutputs>(value)
532 .map_err(serde::de::Error::custom)?,
533 ),
534 "storage" => Box::new(
535 serde_json::from_value::<crate::resources::StorageOutputs>(value)
536 .map_err(serde::de::Error::custom)?,
537 ),
538 "queue" => Box::new(
539 serde_json::from_value::<crate::resources::QueueOutputs>(value)
540 .map_err(serde::de::Error::custom)?,
541 ),
542 "kv" => Box::new(
543 serde_json::from_value::<crate::resources::KvOutputs>(value)
544 .map_err(serde::de::Error::custom)?,
545 ),
546 "postgres" => Box::new(
547 serde_json::from_value::<crate::resources::PostgresOutputs>(value)
548 .map_err(serde::de::Error::custom)?,
549 ),
550 "network" => Box::new(
551 serde_json::from_value::<crate::resources::NetworkOutputs>(value)
552 .map_err(serde::de::Error::custom)?,
553 ),
554 "build" => Box::new(
555 serde_json::from_value::<crate::resources::BuildOutputs>(value)
556 .map_err(serde::de::Error::custom)?,
557 ),
558 "service-account" => Box::new(
559 serde_json::from_value::<crate::resources::ServiceAccountOutputs>(value)
560 .map_err(serde::de::Error::custom)?,
561 ),
562 "artifact-registry" => Box::new(
563 serde_json::from_value::<crate::resources::ArtifactRegistryOutputs>(value)
564 .map_err(serde::de::Error::custom)?,
565 ),
566 "service_activation" => Box::new(
567 serde_json::from_value::<crate::resources::ServiceActivationOutputs>(value)
568 .map_err(serde::de::Error::custom)?,
569 ),
570 "remote-stack-management" => Box::new(
571 serde_json::from_value::<crate::resources::RemoteStackManagementOutputs>(value)
572 .map_err(serde::de::Error::custom)?,
573 ),
574 "kubernetes-cluster" => Box::new(
575 serde_json::from_value::<crate::resources::KubernetesClusterOutputs>(value)
576 .map_err(serde::de::Error::custom)?,
577 ),
578 "azure_resource_group" => Box::new(
579 serde_json::from_value::<crate::resources::AzureResourceGroupOutputs>(value)
580 .map_err(serde::de::Error::custom)?,
581 ),
582 "azure_storage_account" => Box::new(
583 serde_json::from_value::<crate::resources::AzureStorageAccountOutputs>(value)
584 .map_err(serde::de::Error::custom)?,
585 ),
586 "azure_container_apps_environment" => Box::new(
587 serde_json::from_value::<crate::resources::AzureContainerAppsEnvironmentOutputs>(
588 value,
589 )
590 .map_err(serde::de::Error::custom)?,
591 ),
592 "azure_service_bus_namespace" => Box::new(
593 serde_json::from_value::<crate::resources::AzureServiceBusNamespaceOutputs>(value)
594 .map_err(serde::de::Error::custom)?,
595 ),
596 other => {
597 return Err(serde::de::Error::unknown_variant(
598 other,
599 &[
600 "vault",
601 "worker",
602 "daemon",
603 "container",
604 "compute-cluster",
605 "storage",
606 "queue",
607 "kv",
608 "postgres",
609 "network",
610 "build",
611 "service-account",
612 "artifact-registry",
613 "service_activation",
614 "remote-stack-management",
615 "kubernetes-cluster",
616 "azure_resource_group",
617 "azure_storage_account",
618 "azure_container_apps_environment",
619 "azure_service_bus_namespace",
620 ],
621 ))
622 }
623 };
624
625 Ok(ResourceOutputs { inner })
626 }
627}
628
629impl ResourceOutputs {
630 pub fn new<T: ResourceOutputsDefinition>(outputs: T) -> Self {
632 Self {
633 inner: Box::new(outputs),
634 }
635 }
636
637 pub fn as_resource_outputs(&self) -> &dyn ResourceOutputsDefinition {
639 self.inner.as_ref()
640 }
641
642 pub fn downcast_ref<T: ResourceOutputsDefinition + 'static>(&self) -> Option<&T> {
644 self.inner.as_any().downcast_ref::<T>()
645 }
646}
647
648impl PartialEq for ResourceOutputs {
649 fn eq(&self, other: &Self) -> bool {
650 self.inner.outputs_eq(other.inner.as_ref())
651 }
652}
653
654impl Eq for ResourceOutputs {}
655
656#[cfg(feature = "openapi")]
675impl PartialSchema for ResourceOutputs {
676 fn schema() -> RefOr<Schema> {
677 RefOr::T(Schema::Object(
678 ObjectBuilder::new()
679 .schema_type(Type::Object)
680 .property("type", Ref::from_schema_name("ResourceType"))
681 .required("type")
682 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
683 .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."))
684 .build()
685 ))
686 }
687}
688
689#[cfg(feature = "openapi")]
690impl ToSchema for ResourceOutputs {
691 fn name() -> std::borrow::Cow<'static, str> {
692 std::borrow::Cow::Borrowed("BaseResourceOutputs")
693 }
694}
695
696#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
698#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
699#[serde(rename_all = "kebab-case")]
700pub enum ResourceStatus {
701 Pending, Provisioning, ProvisionFailed,
704 Running, Updating,
706 UpdateFailed,
707 Deleting, DeleteFailed,
709 TeardownRequired, Deleted, RefreshFailed, }
713
714impl ResourceStatus {
715 pub fn is_terminal(&self) -> bool {
716 match self {
717 ResourceStatus::TeardownRequired => true,
718 ResourceStatus::Deleted => true,
719 ResourceStatus::ProvisionFailed => true,
720 ResourceStatus::UpdateFailed => true,
721 ResourceStatus::DeleteFailed => true,
722 ResourceStatus::RefreshFailed => true,
723 _ => false, }
725 }
726}
727
728#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Hash, Deserialize)]
730#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
731#[serde(rename_all = "kebab-case")]
732pub enum ResourceLifecycle {
733 Frozen,
737
738 Live,
742}