use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::borrow::Cow;
use std::fmt::Debug;
#[cfg(feature = "openapi")]
use utoipa::openapi::schema::AdditionalProperties;
#[cfg(feature = "openapi")]
use utoipa::openapi::{ObjectBuilder, Ref, RefOr, Schema, Type};
#[cfg(feature = "openapi")]
use utoipa::{PartialSchema, ToSchema};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ResourceType(pub Cow<'static, str>);
impl ResourceType {
pub const fn from_static(s: &'static str) -> Self {
Self(Cow::Borrowed(s))
}
}
impl From<String> for ResourceType {
fn from(s: String) -> Self {
Self(Cow::Owned(s))
}
}
impl From<&str> for ResourceType {
fn from(s: &str) -> Self {
Self(Cow::Owned(s.to_string()))
}
}
impl std::fmt::Display for ResourceType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<ResourceType> for String {
fn from(val: ResourceType) -> Self {
val.0.into_owned()
}
}
impl AsRef<str> for ResourceType {
fn as_ref(&self) -> &str {
&self.0
}
}
#[cfg(feature = "openapi")]
impl PartialSchema for ResourceType {
fn schema() -> RefOr<Schema> {
RefOr::T(Schema::Object(
ObjectBuilder::new()
.schema_type(Type::String)
.description(Some("Resource type identifier that determines the specific kind of resource. This field is used for polymorphic deserialization and resource-specific behavior."))
.examples([
"worker",
"storage",
"queue",
"redis",
"postgres"
])
.build()
))
}
}
#[cfg(feature = "openapi")]
impl ToSchema for ResourceType {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("ResourceType")
}
}
pub trait ResourceDefinition: Debug + Send + Sync + 'static {
fn get_resource_type(&self) -> ResourceType;
fn id(&self) -> &str;
fn get_dependencies(&self) -> Vec<ResourceRef>;
fn get_permissions(&self) -> Option<&str> {
None
}
fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()>;
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn box_clone(&self) -> Box<dyn ResourceDefinition>;
fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool;
fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
}
impl Clone for Box<dyn ResourceDefinition> {
fn clone(&self) -> Self {
self.box_clone()
}
}
#[derive(Debug, Clone)]
pub struct Resource {
inner: Box<dyn ResourceDefinition>,
}
impl Serialize for Resource {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
let mut v = self
.inner
.to_json_value()
.map_err(serde::ser::Error::custom)?;
v.as_object_mut()
.ok_or_else(|| serde::ser::Error::custom("resource must serialize as object"))?
.insert(
"type".into(),
serde_json::Value::String(self.inner.get_resource_type().0.into_owned()),
);
v.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Resource {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
let mut value = serde_json::Value::deserialize(deserializer)?;
let type_tag = value
.get("type")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::missing_field("type"))?
.to_string();
if let Some(obj) = value.as_object_mut() {
obj.remove("type");
}
let inner: Box<dyn ResourceDefinition> = match type_tag.as_str() {
"vault" => Box::new(
serde_json::from_value::<crate::resources::Vault>(value)
.map_err(serde::de::Error::custom)?,
),
"worker" => Box::new(
serde_json::from_value::<crate::resources::Worker>(value)
.map_err(serde::de::Error::custom)?,
),
"daemon" => Box::new(
serde_json::from_value::<crate::resources::Daemon>(value)
.map_err(serde::de::Error::custom)?,
),
"container" => Box::new(
serde_json::from_value::<crate::resources::Container>(value)
.map_err(serde::de::Error::custom)?,
),
"compute-cluster" => Box::new(
serde_json::from_value::<crate::resources::ComputeCluster>(value)
.map_err(serde::de::Error::custom)?,
),
"kubernetes-cluster" => Box::new(
serde_json::from_value::<crate::resources::KubernetesCluster>(value)
.map_err(serde::de::Error::custom)?,
),
"storage" => Box::new(
serde_json::from_value::<crate::resources::Storage>(value)
.map_err(serde::de::Error::custom)?,
),
"queue" => Box::new(
serde_json::from_value::<crate::resources::Queue>(value)
.map_err(serde::de::Error::custom)?,
),
"kv" => Box::new(
serde_json::from_value::<crate::resources::Kv>(value)
.map_err(serde::de::Error::custom)?,
),
"network" => Box::new(
serde_json::from_value::<crate::resources::Network>(value)
.map_err(serde::de::Error::custom)?,
),
"build" => Box::new(
serde_json::from_value::<crate::resources::Build>(value)
.map_err(serde::de::Error::custom)?,
),
"service-account" => Box::new(
serde_json::from_value::<crate::resources::ServiceAccount>(value)
.map_err(serde::de::Error::custom)?,
),
"artifact-registry" => Box::new(
serde_json::from_value::<crate::resources::ArtifactRegistry>(value)
.map_err(serde::de::Error::custom)?,
),
"service_activation" => Box::new(
serde_json::from_value::<crate::resources::ServiceActivation>(value)
.map_err(serde::de::Error::custom)?,
),
"remote-stack-management" => Box::new(
serde_json::from_value::<crate::resources::RemoteStackManagement>(value)
.map_err(serde::de::Error::custom)?,
),
"azure_resource_group" => Box::new(
serde_json::from_value::<crate::resources::AzureResourceGroup>(value)
.map_err(serde::de::Error::custom)?,
),
"azure_storage_account" => Box::new(
serde_json::from_value::<crate::resources::AzureStorageAccount>(value)
.map_err(serde::de::Error::custom)?,
),
"azure_container_apps_environment" => Box::new(
serde_json::from_value::<crate::resources::AzureContainerAppsEnvironment>(value)
.map_err(serde::de::Error::custom)?,
),
"azure_service_bus_namespace" => Box::new(
serde_json::from_value::<crate::resources::AzureServiceBusNamespace>(value)
.map_err(serde::de::Error::custom)?,
),
other => {
return Err(serde::de::Error::unknown_variant(
other,
&[
"vault",
"worker",
"daemon",
"container",
"compute-cluster",
"kubernetes-cluster",
"storage",
"queue",
"kv",
"network",
"build",
"service-account",
"artifact-registry",
"service_activation",
"remote-stack-management",
"azure_resource_group",
"azure_storage_account",
"azure_container_apps_environment",
"azure_service_bus_namespace",
],
))
}
};
Ok(Resource { inner })
}
}
impl Resource {
pub fn new<T: ResourceDefinition>(resource: T) -> Self {
Self {
inner: Box::new(resource),
}
}
pub fn from_boxed(boxed_resource: Box<dyn ResourceDefinition>) -> Self {
Self {
inner: boxed_resource,
}
}
pub fn resource_type(&self) -> ResourceType {
self.inner.get_resource_type()
}
pub fn id(&self) -> &str {
self.inner.id()
}
pub fn get_dependencies(&self) -> Vec<ResourceRef> {
self.inner.get_dependencies()
}
pub fn get_permissions(&self) -> Option<&str> {
self.inner.get_permissions()
}
pub fn validate_update(&self, new_config: &Resource) -> Result<()> {
self.inner.validate_update(new_config.inner.as_ref())
}
pub fn as_resource_definition(&self) -> &dyn ResourceDefinition {
self.inner.as_ref()
}
pub fn downcast_ref<T: ResourceDefinition + 'static>(&self) -> Option<&T> {
self.inner.as_any().downcast_ref::<T>()
}
pub fn downcast_mut<T: ResourceDefinition + 'static>(&mut self) -> Option<&mut T> {
self.inner.as_any_mut().downcast_mut::<T>()
}
}
impl PartialEq for Resource {
fn eq(&self, other: &Self) -> bool {
self.inner.resource_eq(other.inner.as_ref())
}
}
impl Eq for Resource {}
#[cfg(feature = "openapi")]
impl PartialSchema for Resource {
fn schema() -> RefOr<Schema> {
RefOr::T(Schema::Object(
ObjectBuilder::new()
.schema_type(Type::Object)
.property("type", Ref::from_schema_name("ResourceType"))
.property("id",
ObjectBuilder::new()
.schema_type(Type::String)
.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."))
.build()
)
.required("type")
.required("id")
.additional_properties(Some(AdditionalProperties::FreeForm(true)))
.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."))
.build()
))
}
}
#[cfg(feature = "openapi")]
impl ToSchema for Resource {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("BaseResource")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct ResourceRef {
#[serde(rename = "type")]
pub resource_type: ResourceType,
pub id: String,
}
impl ResourceRef {
pub fn new(resource_type: ResourceType, id: impl Into<String>) -> Self {
Self {
resource_type,
id: id.into(),
}
}
pub fn resource_type(&self) -> &ResourceType {
&self.resource_type
}
pub fn id(&self) -> &str {
&self.id
}
}
impl<T: ResourceDefinition> From<&T> for ResourceRef {
fn from(resource: &T) -> Self {
Self::new(resource.get_resource_type(), resource.id())
}
}
impl From<&Resource> for ResourceRef {
fn from(resource: &Resource) -> Self {
Self::new(resource.resource_type(), resource.id())
}
}
pub trait ResourceOutputsDefinition: Debug + Send + Sync + 'static {
fn get_resource_type(&self) -> ResourceType;
fn as_any(&self) -> &dyn Any;
fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition>;
fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool;
fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
}
impl Clone for Box<dyn ResourceOutputsDefinition> {
fn clone(&self) -> Self {
self.box_clone()
}
}
#[derive(Debug, Clone)]
pub struct ResourceOutputs {
inner: Box<dyn ResourceOutputsDefinition>,
}
impl Serialize for ResourceOutputs {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
let mut v = self
.inner
.to_json_value()
.map_err(serde::ser::Error::custom)?;
v.as_object_mut()
.ok_or_else(|| serde::ser::Error::custom("resource outputs must serialize as object"))?
.insert(
"type".into(),
serde_json::Value::String(self.inner.get_resource_type().0.into_owned()),
);
v.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ResourceOutputs {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
let mut value = serde_json::Value::deserialize(deserializer)?;
let type_tag = value
.get("type")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::missing_field("type"))?
.to_string();
if let Some(obj) = value.as_object_mut() {
obj.remove("type");
}
let inner: Box<dyn ResourceOutputsDefinition> = match type_tag.as_str() {
"vault" => Box::new(
serde_json::from_value::<crate::resources::VaultOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"worker" => Box::new(
serde_json::from_value::<crate::resources::WorkerOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"daemon" => Box::new(
serde_json::from_value::<crate::resources::DaemonOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"container" => Box::new(
serde_json::from_value::<crate::resources::ContainerOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"compute-cluster" => Box::new(
serde_json::from_value::<crate::resources::ComputeClusterOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"storage" => Box::new(
serde_json::from_value::<crate::resources::StorageOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"queue" => Box::new(
serde_json::from_value::<crate::resources::QueueOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"kv" => Box::new(
serde_json::from_value::<crate::resources::KvOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"network" => Box::new(
serde_json::from_value::<crate::resources::NetworkOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"build" => Box::new(
serde_json::from_value::<crate::resources::BuildOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"service-account" => Box::new(
serde_json::from_value::<crate::resources::ServiceAccountOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"artifact-registry" => Box::new(
serde_json::from_value::<crate::resources::ArtifactRegistryOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"service_activation" => Box::new(
serde_json::from_value::<crate::resources::ServiceActivationOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"remote-stack-management" => Box::new(
serde_json::from_value::<crate::resources::RemoteStackManagementOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"kubernetes-cluster" => Box::new(
serde_json::from_value::<crate::resources::KubernetesClusterOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"azure_resource_group" => Box::new(
serde_json::from_value::<crate::resources::AzureResourceGroupOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"azure_storage_account" => Box::new(
serde_json::from_value::<crate::resources::AzureStorageAccountOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
"azure_container_apps_environment" => Box::new(
serde_json::from_value::<crate::resources::AzureContainerAppsEnvironmentOutputs>(
value,
)
.map_err(serde::de::Error::custom)?,
),
"azure_service_bus_namespace" => Box::new(
serde_json::from_value::<crate::resources::AzureServiceBusNamespaceOutputs>(value)
.map_err(serde::de::Error::custom)?,
),
other => {
return Err(serde::de::Error::unknown_variant(
other,
&[
"vault",
"worker",
"daemon",
"container",
"compute-cluster",
"storage",
"queue",
"kv",
"network",
"build",
"service-account",
"artifact-registry",
"service_activation",
"remote-stack-management",
"kubernetes-cluster",
"azure_resource_group",
"azure_storage_account",
"azure_container_apps_environment",
"azure_service_bus_namespace",
],
))
}
};
Ok(ResourceOutputs { inner })
}
}
impl ResourceOutputs {
pub fn new<T: ResourceOutputsDefinition>(outputs: T) -> Self {
Self {
inner: Box::new(outputs),
}
}
pub fn as_resource_outputs(&self) -> &dyn ResourceOutputsDefinition {
self.inner.as_ref()
}
pub fn downcast_ref<T: ResourceOutputsDefinition + 'static>(&self) -> Option<&T> {
self.inner.as_any().downcast_ref::<T>()
}
}
impl PartialEq for ResourceOutputs {
fn eq(&self, other: &Self) -> bool {
self.inner.outputs_eq(other.inner.as_ref())
}
}
impl Eq for ResourceOutputs {}
#[cfg(feature = "openapi")]
impl PartialSchema for ResourceOutputs {
fn schema() -> RefOr<Schema> {
RefOr::T(Schema::Object(
ObjectBuilder::new()
.schema_type(Type::Object)
.property("type", Ref::from_schema_name("ResourceType"))
.required("type")
.additional_properties(Some(AdditionalProperties::FreeForm(true)))
.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."))
.build()
))
}
}
#[cfg(feature = "openapi")]
impl ToSchema for ResourceOutputs {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("BaseResourceOutputs")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "kebab-case")]
pub enum ResourceStatus {
Pending, Provisioning, ProvisionFailed,
Running, Updating,
UpdateFailed,
Deleting, DeleteFailed,
TeardownRequired, Deleted, RefreshFailed, }
impl ResourceStatus {
pub fn is_terminal(&self) -> bool {
match self {
ResourceStatus::TeardownRequired => true,
ResourceStatus::Deleted => true,
ResourceStatus::ProvisionFailed => true,
ResourceStatus::UpdateFailed => true,
ResourceStatus::DeleteFailed => true,
ResourceStatus::RefreshFailed => true,
_ => false, }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Hash, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "kebab-case")]
pub enum ResourceLifecycle {
Frozen,
Live,
}