Skip to main content

alien_core/resources/
remote_stack_management.rs

1use 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/// Represents cross-account management access configuration for a stack deployed
10/// on AWS, GCP, or Azure platforms. This resource sets up the necessary IAM/RBAC
11/// configuration to allow another cloud account to manage the stack.
12///
13/// Maps to:
14/// - AWS: Cross-account IAM role with management permissions
15/// - GCP: Service account with management permissions and impersonation rights
16/// - Azure: User-assigned managed identity with federated credential and custom RBAC
17///
18/// This resource is automatically created for AWS, GCP, and Azure platforms
19/// when the stack needs to be managed by another account. The management account
20/// and identity information comes from the platform configuration.
21#[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    /// Identifier for the remote stack management. Must contain only alphanumeric characters, hyphens, and underscores ([A-Za-z0-9-_]).
27    /// Maximum 64 characters.
28    #[builder(start_fn)]
29    pub id: String,
30}
31
32impl RemoteStackManagement {
33    /// The resource type identifier for RemoteStackManagement
34    pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("remote-stack-management");
35
36    /// Returns the remote stack management's unique identifier.
37    pub fn id(&self) -> &str {
38        &self.id
39    }
40}
41
42/// Resource outputs for RemoteStackManagement.
43/// Different platforms will provide different outputs based on their implementation.
44#[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    /// Platform-specific management resource identifier
49    /// For AWS: The ARN of the created cross-account role
50    /// For GCP: The email of the created service account
51    /// For Azure: The resource ID of the target user-assigned managed identity
52    pub management_resource_id: String,
53
54    /// Platform-specific access configuration
55    /// For AWS: The role ARN to assume
56    /// For GCP: The service account email to impersonate
57    /// For Azure: JSON containing the target managed identity client ID and tenant ID
58    pub access_configuration: String,
59
60    /// Read-only compatibility for deployments imported before Remote Bindings became a
61    /// first-class resource. New controllers never serialize this field.
62    #[serde(default, rename = "remoteBindingsAccess", skip_serializing)]
63    #[cfg_attr(feature = "openapi", schema(ignore))]
64    pub legacy_remote_bindings_access: Option<RemoteBindingsOutputs>,
65}
66
67// Implementation of ResourceDefinition trait for RemoteStackManagement
68impl 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        // RemoteStackManagement typically doesn't depend on other resources,
79        // but may depend on infrastructure requirements like resource groups
80        Vec::new()
81    }
82
83    fn validate_update(&self, new_config: &dyn ResourceDefinition) -> crate::error::Result<()> {
84        // Try to downcast to RemoteStackManagement for type-specific validation
85        if let Some(new_remote_mgmt) = new_config.as_any().downcast_ref::<RemoteStackManagement>() {
86            // Validate that the ID matches
87            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            // RemoteStackManagement configuration can be updated
97            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}