Skip to main content

alien_core/resources/
kubernetes_cluster.rs

1//! KubernetesCluster resource for Kubernetes runtime substrates.
2
3use crate::{
4    error::{ErrorData, Result},
5    import::data::AzureApplicationGatewayForContainersBootstrap,
6    resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef},
7    ResourceType,
8};
9use alien_error::AlienError;
10use bon::Builder;
11use serde::{Deserialize, Serialize};
12use std::any::Any;
13
14/// Kubernetes provider backing the runtime substrate.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
17#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
18#[serde(rename_all = "camelCase")]
19pub enum KubernetesClusterProvider {
20    Eks,
21    Gke,
22    Aks,
23    Generic,
24}
25
26/// Ownership model for the Kubernetes cluster.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
29#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
30#[serde(rename_all = "camelCase")]
31pub enum KubernetesClusterOwnership {
32    Managed,
33    Existing,
34    External,
35}
36
37/// How Alien should heartbeat this Kubernetes substrate.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
40#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
41#[serde(rename_all = "camelCase")]
42pub enum KubernetesHeartbeatMode {
43    KubernetesApi,
44    KubernetesApiAndCloudMetadata,
45    Disabled,
46}
47
48/// Optional provider-specific identity for a cloud-backed Kubernetes cluster.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
50#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
51#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub struct KubernetesCloudReference {
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub cluster_name: Option<String>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub cluster_id: Option<String>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub region: Option<String>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub account_id: Option<String>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub project_id: Option<String>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub subscription_id: Option<String>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub resource_group: Option<String>,
68}
69
70/// Runtime substrate for Kubernetes deployments.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
72#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
73#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
74#[serde(rename_all = "camelCase", deny_unknown_fields)]
75#[builder(start_fn = new)]
76pub struct KubernetesCluster {
77    #[builder(start_fn)]
78    pub id: String,
79    pub provider: KubernetesClusterProvider,
80    pub ownership: KubernetesClusterOwnership,
81    pub namespace: String,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub cloud: Option<KubernetesCloudReference>,
84    pub heartbeat_mode: KubernetesHeartbeatMode,
85}
86
87impl KubernetesCluster {
88    pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("kubernetes-cluster");
89
90    pub fn id(&self) -> &str {
91        &self.id
92    }
93}
94
95/// Outputs produced once the Kubernetes substrate is ready for workloads.
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
98#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
99#[serde(rename_all = "camelCase")]
100pub struct KubernetesClusterOutputs {
101    pub provider: KubernetesClusterProvider,
102    pub ownership: KubernetesClusterOwnership,
103    pub namespace: String,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub cluster_name: Option<String>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub cluster_id: Option<String>,
108    pub kubernetes_api_reachable: bool,
109    pub namespace_ready: bool,
110    pub rbac_ready: bool,
111    pub agent_ready: bool,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub cloud_metadata_ready: Option<bool>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub azure_application_gateway_for_containers:
116        Option<AzureApplicationGatewayForContainersBootstrap>,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub version: Option<String>,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub status_message: Option<String>,
121}
122
123impl ResourceOutputsDefinition for KubernetesClusterOutputs {
124    fn get_resource_type(&self) -> ResourceType {
125        KubernetesCluster::RESOURCE_TYPE
126    }
127
128    fn as_any(&self) -> &dyn Any {
129        self
130    }
131
132    fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
133        Box::new(self.clone())
134    }
135
136    fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
137        other.as_any().downcast_ref::<KubernetesClusterOutputs>() == Some(self)
138    }
139
140    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
141        serde_json::to_value(self)
142    }
143}
144
145impl ResourceDefinition for KubernetesCluster {
146    fn get_resource_type(&self) -> ResourceType {
147        Self::RESOURCE_TYPE
148    }
149
150    fn id(&self) -> &str {
151        &self.id
152    }
153
154    fn get_dependencies(&self) -> Vec<ResourceRef> {
155        Vec::new()
156    }
157
158    fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
159        let new_cluster = new_config
160            .as_any()
161            .downcast_ref::<KubernetesCluster>()
162            .ok_or_else(|| {
163                AlienError::new(ErrorData::UnexpectedResourceType {
164                    resource_id: self.id.clone(),
165                    expected: Self::RESOURCE_TYPE,
166                    actual: new_config.get_resource_type(),
167                })
168            })?;
169
170        if self != new_cluster {
171            return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
172                resource_id: self.id.clone(),
173                reason: "KubernetesCluster is a frozen runtime substrate and cannot be changed during runtime updates".to_string(),
174            }));
175        }
176
177        Ok(())
178    }
179
180    fn as_any(&self) -> &dyn Any {
181        self
182    }
183
184    fn as_any_mut(&mut self) -> &mut dyn Any {
185        self
186    }
187
188    fn box_clone(&self) -> Box<dyn ResourceDefinition> {
189        Box::new(self.clone())
190    }
191
192    fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
193        other.as_any().downcast_ref::<KubernetesCluster>() == Some(self)
194    }
195
196    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
197        serde_json::to_value(self)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn generic_external_cluster_serializes_as_resource() {
207        let cluster = KubernetesCluster::new("kubernetes".to_string())
208            .provider(KubernetesClusterProvider::Generic)
209            .ownership(KubernetesClusterOwnership::External)
210            .namespace("default".to_string())
211            .heartbeat_mode(KubernetesHeartbeatMode::KubernetesApi)
212            .build();
213
214        let resource = crate::Resource::new(cluster);
215        let value = serde_json::to_value(&resource).unwrap();
216
217        assert_eq!(value["type"], "kubernetes-cluster");
218        assert_eq!(value["provider"], "generic");
219        assert_eq!(value["ownership"], "external");
220    }
221}