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 "key" => Box::new(
202 serde_json::from_value::<crate::resources::Key>(value)
203 .map_err(serde::de::Error::custom)?,
204 ),
205 "queue" => Box::new(
206 serde_json::from_value::<crate::resources::Queue>(value)
207 .map_err(serde::de::Error::custom)?,
208 ),
209 "sandbox" => Box::new(
210 serde_json::from_value::<crate::resources::Sandbox>(value)
211 .map_err(serde::de::Error::custom)?,
212 ),
213 "email" => Box::new(
214 serde_json::from_value::<crate::resources::Email>(value)
215 .map_err(serde::de::Error::custom)?,
216 ),
217 "kv" => Box::new(
218 serde_json::from_value::<crate::resources::Kv>(value)
219 .map_err(serde::de::Error::custom)?,
220 ),
221 "postgres" => Box::new(
222 serde_json::from_value::<crate::resources::Postgres>(value)
223 .map_err(serde::de::Error::custom)?,
224 ),
225 "ai" => Box::new(
226 serde_json::from_value::<crate::resources::Ai>(value)
227 .map_err(serde::de::Error::custom)?,
228 ),
229 "network" => Box::new(
230 serde_json::from_value::<crate::resources::Network>(value)
231 .map_err(serde::de::Error::custom)?,
232 ),
233 "build" => Box::new(
234 serde_json::from_value::<crate::resources::Build>(value)
235 .map_err(serde::de::Error::custom)?,
236 ),
237 "service-account" => Box::new(
238 serde_json::from_value::<crate::resources::ServiceAccount>(value)
239 .map_err(serde::de::Error::custom)?,
240 ),
241 "artifact-registry" => Box::new(
242 serde_json::from_value::<crate::resources::ArtifactRegistry>(value)
243 .map_err(serde::de::Error::custom)?,
244 ),
245 "service_activation" => Box::new(
246 serde_json::from_value::<crate::resources::ServiceActivation>(value)
247 .map_err(serde::de::Error::custom)?,
248 ),
249 "remote-stack-management" => Box::new(
250 serde_json::from_value::<crate::resources::RemoteStackManagement>(value)
251 .map_err(serde::de::Error::custom)?,
252 ),
253 "resource-access" => Box::new(
254 serde_json::from_value::<crate::resources::RemoteBindings>(value)
255 .map_err(serde::de::Error::custom)?,
256 ),
257 "azure_resource_group" => Box::new(
258 serde_json::from_value::<crate::resources::AzureResourceGroup>(value)
259 .map_err(serde::de::Error::custom)?,
260 ),
261 "gcp_agent_platform_engine" => Box::new(
262 serde_json::from_value::<crate::resources::GcpAgentPlatformEngine>(value)
263 .map_err(serde::de::Error::custom)?,
264 ),
265 "azure_storage_account" => Box::new(
266 serde_json::from_value::<crate::resources::AzureStorageAccount>(value)
267 .map_err(serde::de::Error::custom)?,
268 ),
269 "azure_container_apps_environment" => Box::new(
270 serde_json::from_value::<crate::resources::AzureContainerAppsEnvironment>(value)
271 .map_err(serde::de::Error::custom)?,
272 ),
273 "azure_service_bus_namespace" => Box::new(
274 serde_json::from_value::<crate::resources::AzureServiceBusNamespace>(value)
275 .map_err(serde::de::Error::custom)?,
276 ),
277 "experimental/aws-opensearch" => Box::new(
278 serde_json::from_value::<crate::resources::AwsOpenSearch>(value)
279 .map_err(serde::de::Error::custom)?,
280 ),
281 other => {
282 return Err(serde::de::Error::unknown_variant(
283 other,
284 &[
285 "vault",
286 "worker",
287 "daemon",
288 "container",
289 "compute-cluster",
290 "kubernetes-cluster",
291 "storage",
292 "key",
293 "queue",
294 "sandbox",
295 "email",
296 "kv",
297 "postgres",
298 "ai",
299 "network",
300 "build",
301 "service-account",
302 "artifact-registry",
303 "service_activation",
304 "remote-stack-management",
305 "resource-access",
306 "azure_resource_group",
307 "gcp_agent_platform_engine",
308 "azure_storage_account",
309 "azure_container_apps_environment",
310 "azure_service_bus_namespace",
311 "experimental/aws-opensearch",
312 ],
313 ))
314 }
315 };
316
317 Ok(Resource { inner })
318 }
319}
320
321impl Resource {
322 pub fn new<T: ResourceDefinition>(resource: T) -> Self {
324 Self {
325 inner: Box::new(resource),
326 }
327 }
328
329 pub fn from_boxed(boxed_resource: Box<dyn ResourceDefinition>) -> Self {
331 Self {
332 inner: boxed_resource,
333 }
334 }
335
336 pub fn resource_type(&self) -> ResourceType {
338 self.inner.get_resource_type()
339 }
340
341 pub fn id(&self) -> &str {
343 self.inner.id()
344 }
345
346 pub fn get_dependencies(&self) -> Vec<ResourceRef> {
348 self.inner.get_dependencies()
349 }
350
351 pub fn get_permissions(&self) -> Option<&str> {
353 self.inner.get_permissions()
354 }
355
356 pub fn validate_update(&self, new_config: &Resource) -> Result<()> {
358 self.inner.validate_update(new_config.inner.as_ref())
359 }
360
361 pub fn as_resource_definition(&self) -> &dyn ResourceDefinition {
363 self.inner.as_ref()
364 }
365
366 pub fn downcast_ref<T: ResourceDefinition + 'static>(&self) -> Option<&T> {
368 self.inner.as_any().downcast_ref::<T>()
369 }
370
371 pub fn downcast_mut<T: ResourceDefinition + 'static>(&mut self) -> Option<&mut T> {
373 self.inner.as_any_mut().downcast_mut::<T>()
374 }
375}
376
377impl PartialEq for Resource {
378 fn eq(&self, other: &Self) -> bool {
379 self.inner.resource_eq(other.inner.as_ref())
380 }
381}
382
383impl Eq for Resource {}
384
385#[cfg(feature = "openapi")]
407impl PartialSchema for Resource {
408 fn schema() -> RefOr<Schema> {
409 RefOr::T(Schema::Object(
410 ObjectBuilder::new()
411 .schema_type(Type::Object)
412 .property("type", Ref::from_schema_name("ResourceType"))
413 .property("id",
414 ObjectBuilder::new()
415 .schema_type(Type::String)
416 .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."))
417 .build()
418 )
419 .required("type")
420 .required("id")
421 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
422 .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."))
423 .build()
424 ))
425 }
426}
427
428#[cfg(feature = "openapi")]
429impl ToSchema for Resource {
430 fn name() -> std::borrow::Cow<'static, str> {
431 std::borrow::Cow::Borrowed("BaseResource")
432 }
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
437#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
438#[serde(rename_all = "camelCase")]
439pub struct ResourceRef {
440 #[serde(rename = "type")]
441 pub resource_type: ResourceType,
442 pub id: String,
443}
444
445impl ResourceRef {
446 pub fn new(resource_type: ResourceType, id: impl Into<String>) -> Self {
448 Self {
449 resource_type,
450 id: id.into(),
451 }
452 }
453
454 pub fn resource_type(&self) -> &ResourceType {
456 &self.resource_type
457 }
458
459 pub fn id(&self) -> &str {
461 &self.id
462 }
463}
464
465impl<T: ResourceDefinition> From<&T> for ResourceRef {
466 fn from(resource: &T) -> Self {
467 Self::new(resource.get_resource_type(), resource.id())
468 }
469}
470
471impl From<&Resource> for ResourceRef {
472 fn from(resource: &Resource) -> Self {
473 Self::new(resource.resource_type(), resource.id())
474 }
475}
476
477pub trait ResourceOutputsDefinition: Debug + Send + Sync + 'static {
481 fn get_resource_type(&self) -> ResourceType;
483
484 fn as_any(&self) -> &dyn Any;
486
487 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition>;
489
490 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool;
492
493 fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
495}
496
497impl Clone for Box<dyn ResourceOutputsDefinition> {
499 fn clone(&self) -> Self {
500 self.box_clone()
501 }
502}
503
504#[derive(Debug, Clone)]
507pub struct ResourceOutputs {
508 inner: Box<dyn ResourceOutputsDefinition>,
509}
510
511impl Serialize for ResourceOutputs {
512 fn serialize<S: serde::Serializer>(
513 &self,
514 serializer: S,
515 ) -> std::result::Result<S::Ok, S::Error> {
516 let mut v = self
517 .inner
518 .to_json_value()
519 .map_err(serde::ser::Error::custom)?;
520 v.as_object_mut()
521 .ok_or_else(|| serde::ser::Error::custom("resource outputs must serialize as object"))?
522 .insert(
523 "type".into(),
524 serde_json::Value::String(self.inner.get_resource_type().0.into_owned()),
525 );
526 v.serialize(serializer)
527 }
528}
529
530impl<'de> Deserialize<'de> for ResourceOutputs {
531 fn deserialize<D: serde::Deserializer<'de>>(
532 deserializer: D,
533 ) -> std::result::Result<Self, D::Error> {
534 let mut value = serde_json::Value::deserialize(deserializer)?;
535 let type_tag = value
536 .get("type")
537 .and_then(|v| v.as_str())
538 .ok_or_else(|| serde::de::Error::missing_field("type"))?
539 .to_string();
540
541 if let Some(obj) = value.as_object_mut() {
544 obj.remove("type");
545 }
546
547 let inner: Box<dyn ResourceOutputsDefinition> = match type_tag.as_str() {
548 "vault" => Box::new(
549 serde_json::from_value::<crate::resources::VaultOutputs>(value)
550 .map_err(serde::de::Error::custom)?,
551 ),
552 "worker" => Box::new(
553 serde_json::from_value::<crate::resources::WorkerOutputs>(value)
554 .map_err(serde::de::Error::custom)?,
555 ),
556 "daemon" => Box::new(
557 serde_json::from_value::<crate::resources::DaemonOutputs>(value)
558 .map_err(serde::de::Error::custom)?,
559 ),
560 "container" => Box::new(
561 serde_json::from_value::<crate::resources::ContainerOutputs>(value)
562 .map_err(serde::de::Error::custom)?,
563 ),
564 "compute-cluster" => Box::new(
565 serde_json::from_value::<crate::resources::ComputeClusterOutputs>(value)
566 .map_err(serde::de::Error::custom)?,
567 ),
568 "storage" => Box::new(
569 serde_json::from_value::<crate::resources::StorageOutputs>(value)
570 .map_err(serde::de::Error::custom)?,
571 ),
572 "key" => Box::new(
573 serde_json::from_value::<crate::resources::KeyOutputs>(value)
574 .map_err(serde::de::Error::custom)?,
575 ),
576 "queue" => Box::new(
577 serde_json::from_value::<crate::resources::QueueOutputs>(value)
578 .map_err(serde::de::Error::custom)?,
579 ),
580 "sandbox" => Box::new(
581 serde_json::from_value::<crate::resources::SandboxOutputs>(value)
582 .map_err(serde::de::Error::custom)?,
583 ),
584 "email" => Box::new(
585 serde_json::from_value::<crate::resources::EmailOutputs>(value)
586 .map_err(serde::de::Error::custom)?,
587 ),
588 "kv" => Box::new(
589 serde_json::from_value::<crate::resources::KvOutputs>(value)
590 .map_err(serde::de::Error::custom)?,
591 ),
592 "postgres" => Box::new(
593 serde_json::from_value::<crate::resources::PostgresOutputs>(value)
594 .map_err(serde::de::Error::custom)?,
595 ),
596 "ai" => Box::new(
597 serde_json::from_value::<crate::resources::AiOutputs>(value)
598 .map_err(serde::de::Error::custom)?,
599 ),
600 "network" => Box::new(
601 serde_json::from_value::<crate::resources::NetworkOutputs>(value)
602 .map_err(serde::de::Error::custom)?,
603 ),
604 "build" => Box::new(
605 serde_json::from_value::<crate::resources::BuildOutputs>(value)
606 .map_err(serde::de::Error::custom)?,
607 ),
608 "service-account" => Box::new(
609 serde_json::from_value::<crate::resources::ServiceAccountOutputs>(value)
610 .map_err(serde::de::Error::custom)?,
611 ),
612 "artifact-registry" => Box::new(
613 serde_json::from_value::<crate::resources::ArtifactRegistryOutputs>(value)
614 .map_err(serde::de::Error::custom)?,
615 ),
616 "service_activation" => Box::new(
617 serde_json::from_value::<crate::resources::ServiceActivationOutputs>(value)
618 .map_err(serde::de::Error::custom)?,
619 ),
620 "remote-stack-management" => Box::new(
621 serde_json::from_value::<crate::resources::RemoteStackManagementOutputs>(value)
622 .map_err(serde::de::Error::custom)?,
623 ),
624 "resource-access" => Box::new(
625 serde_json::from_value::<crate::resources::RemoteBindingsOutputs>(value)
626 .map_err(serde::de::Error::custom)?,
627 ),
628 "kubernetes-cluster" => Box::new(
629 serde_json::from_value::<crate::resources::KubernetesClusterOutputs>(value)
630 .map_err(serde::de::Error::custom)?,
631 ),
632 "azure_resource_group" => Box::new(
633 serde_json::from_value::<crate::resources::AzureResourceGroupOutputs>(value)
634 .map_err(serde::de::Error::custom)?,
635 ),
636 "azure_storage_account" => Box::new(
637 serde_json::from_value::<crate::resources::AzureStorageAccountOutputs>(value)
638 .map_err(serde::de::Error::custom)?,
639 ),
640 "azure_container_apps_environment" => Box::new(
641 serde_json::from_value::<crate::resources::AzureContainerAppsEnvironmentOutputs>(
642 value,
643 )
644 .map_err(serde::de::Error::custom)?,
645 ),
646 "azure_service_bus_namespace" => Box::new(
647 serde_json::from_value::<crate::resources::AzureServiceBusNamespaceOutputs>(value)
648 .map_err(serde::de::Error::custom)?,
649 ),
650 "experimental/aws-opensearch" => Box::new(
651 serde_json::from_value::<crate::resources::AwsOpenSearchOutputs>(value)
652 .map_err(serde::de::Error::custom)?,
653 ),
654 other => {
655 return Err(serde::de::Error::unknown_variant(
656 other,
657 &[
658 "vault",
659 "worker",
660 "daemon",
661 "container",
662 "compute-cluster",
663 "storage",
664 "key",
665 "queue",
666 "sandbox",
667 "email",
668 "kv",
669 "postgres",
670 "ai",
671 "network",
672 "build",
673 "service-account",
674 "artifact-registry",
675 "service_activation",
676 "remote-stack-management",
677 "resource-access",
678 "kubernetes-cluster",
679 "azure_resource_group",
680 "azure_storage_account",
681 "azure_container_apps_environment",
682 "azure_service_bus_namespace",
683 "experimental/aws-opensearch",
684 ],
685 ))
686 }
687 };
688
689 Ok(ResourceOutputs { inner })
690 }
691}
692
693impl ResourceOutputs {
694 pub fn new<T: ResourceOutputsDefinition>(outputs: T) -> Self {
696 Self {
697 inner: Box::new(outputs),
698 }
699 }
700
701 pub fn as_resource_outputs(&self) -> &dyn ResourceOutputsDefinition {
703 self.inner.as_ref()
704 }
705
706 pub fn downcast_ref<T: ResourceOutputsDefinition + 'static>(&self) -> Option<&T> {
708 self.inner.as_any().downcast_ref::<T>()
709 }
710}
711
712impl PartialEq for ResourceOutputs {
713 fn eq(&self, other: &Self) -> bool {
714 self.inner.outputs_eq(other.inner.as_ref())
715 }
716}
717
718impl Eq for ResourceOutputs {}
719
720#[cfg(feature = "openapi")]
739impl PartialSchema for ResourceOutputs {
740 fn schema() -> RefOr<Schema> {
741 RefOr::T(Schema::Object(
742 ObjectBuilder::new()
743 .schema_type(Type::Object)
744 .property("type", Ref::from_schema_name("ResourceType"))
745 .required("type")
746 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
747 .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."))
748 .build()
749 ))
750 }
751}
752
753#[cfg(feature = "openapi")]
754impl ToSchema for ResourceOutputs {
755 fn name() -> std::borrow::Cow<'static, str> {
756 std::borrow::Cow::Borrowed("BaseResourceOutputs")
757 }
758}
759
760#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
762#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
763#[serde(rename_all = "kebab-case")]
764pub enum ResourceStatus {
765 Pending, Provisioning, ProvisionFailed,
768 Running, Updating,
770 UpdateFailed,
771 Deleting, DeleteFailed,
773 TeardownRequired, Deleted, RefreshFailed, }
777
778impl ResourceStatus {
779 pub fn is_terminal(&self) -> bool {
780 match self {
781 ResourceStatus::TeardownRequired => true,
782 ResourceStatus::Deleted => true,
783 ResourceStatus::ProvisionFailed => true,
784 ResourceStatus::UpdateFailed => true,
785 ResourceStatus::DeleteFailed => true,
786 ResourceStatus::RefreshFailed => true,
787 _ => false, }
789 }
790}
791
792#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Hash, Deserialize)]
794#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
795#[serde(rename_all = "kebab-case")]
796pub enum ResourceLifecycle {
797 Frozen,
801
802 Live,
806}