alien_core/resources/
remote_stack_management.rs1use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef, ResourceType};
2use crate::RemoteBindingsOutputs;
3use alien_error::AlienError;
4use bon::Builder;
5use serde::{Deserialize, Serialize};
6use std::any::Any;
7use std::fmt::Debug;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
22#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
23#[serde(rename_all = "camelCase", deny_unknown_fields)]
24#[builder(start_fn = new)]
25pub struct RemoteStackManagement {
26 #[builder(start_fn)]
29 pub id: String,
30}
31
32impl RemoteStackManagement {
33 pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("remote-stack-management");
35
36 pub fn id(&self) -> &str {
38 &self.id
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
46#[serde(rename_all = "camelCase", deny_unknown_fields)]
47pub struct RemoteStackManagementOutputs {
48 pub management_resource_id: String,
53
54 pub access_configuration: String,
59
60 #[serde(default, rename = "remoteBindingsAccess", skip_serializing)]
63 #[cfg_attr(feature = "openapi", schema(ignore))]
64 pub legacy_remote_bindings_access: Option<RemoteBindingsOutputs>,
65}
66
67impl ResourceDefinition for RemoteStackManagement {
69 fn get_resource_type(&self) -> ResourceType {
70 Self::RESOURCE_TYPE
71 }
72
73 fn id(&self) -> &str {
74 &self.id
75 }
76
77 fn get_dependencies(&self) -> Vec<ResourceRef> {
78 Vec::new()
81 }
82
83 fn validate_update(&self, new_config: &dyn ResourceDefinition) -> crate::error::Result<()> {
84 if let Some(new_remote_mgmt) = new_config.as_any().downcast_ref::<RemoteStackManagement>() {
86 if self.id != new_remote_mgmt.id {
88 return Err(AlienError::new(
89 crate::error::ErrorData::InvalidResourceUpdate {
90 resource_id: self.id.clone(),
91 reason: "the 'id' field is immutable".to_string(),
92 },
93 ));
94 }
95
96 Ok(())
98 } else {
99 Err(AlienError::new(
100 crate::error::ErrorData::UnexpectedResourceType {
101 resource_id: self.id.clone(),
102 expected: Self::RESOURCE_TYPE,
103 actual: new_config.get_resource_type(),
104 },
105 ))
106 }
107 }
108
109 fn as_any(&self) -> &dyn Any {
110 self
111 }
112
113 fn as_any_mut(&mut self) -> &mut dyn Any {
114 self
115 }
116
117 fn box_clone(&self) -> Box<dyn ResourceDefinition> {
118 Box::new(self.clone())
119 }
120
121 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
122 other
123 .as_any()
124 .downcast_ref::<RemoteStackManagement>()
125 .map(|other_remote_mgmt| self == other_remote_mgmt)
126 .unwrap_or(false)
127 }
128
129 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
130 serde_json::to_value(self)
131 }
132}
133
134impl ResourceOutputsDefinition for RemoteStackManagementOutputs {
135 fn get_resource_type(&self) -> ResourceType {
136 RemoteStackManagement::RESOURCE_TYPE.clone()
137 }
138
139 fn as_any(&self) -> &dyn Any {
140 self
141 }
142
143 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
144 Box::new(self.clone())
145 }
146
147 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
148 other
149 .as_any()
150 .downcast_ref::<RemoteStackManagementOutputs>()
151 == Some(self)
152 }
153
154 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
155 serde_json::to_value(self)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::RemoteStackManagementOutputs;
162
163 #[test]
164 fn legacy_remote_bindings_output_remains_readable_but_is_not_reemitted() {
165 let outputs: RemoteStackManagementOutputs = serde_json::from_value(serde_json::json!({
166 "managementResourceId": "management",
167 "accessConfiguration": "management-access",
168 "remoteBindingsAccess": {
169 "resourceId": "legacy-bindings",
170 "accessConfiguration": "legacy-access"
171 }
172 }))
173 .expect("legacy persisted output must remain readable");
174 assert_eq!(
175 outputs
176 .legacy_remote_bindings_access
177 .as_ref()
178 .map(|access| access.resource_id.as_str()),
179 Some("legacy-bindings")
180 );
181 assert!(
182 serde_json::to_value(outputs)
183 .expect("serialize")
184 .get("remoteBindingsAccess")
185 .is_none(),
186 "new state must not keep writing the legacy ownership slot"
187 );
188 }
189}