Skip to main content

alien_core/
stack.rs

1use crate::permissions::{ManagementPermissions, PermissionProfile, PermissionsConfig};
2use crate::{Resource, ResourceLifecycle, ResourceRef};
3use bon::Builder;
4use indexmap::IndexMap;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
9#[serde(rename_all = "camelCase", deny_unknown_fields)]
10pub struct ResourceEntry {
11    /// Resource configuration (can be any type of resource)
12    pub config: Resource,
13    /// Lifecycle management configuration for this resource
14    pub lifecycle: ResourceLifecycle,
15    /// Additional dependencies for this resource beyond those defined in the resource itself.
16    /// The total dependencies are: resource.get_dependencies() + this list
17    pub dependencies: Vec<ResourceRef>,
18    /// Enable remote bindings for this resource (BYOB use case).
19    /// When true, binding params are synced to StackState's `remote_binding_params`.
20    /// Default: false (prevents sensitive data in synced state).
21    #[serde(default)]
22    pub remote_access: bool,
23}
24
25/// A bag of resources, unaware of any cloud.
26#[derive(Builder, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
28#[serde(rename_all = "camelCase", deny_unknown_fields)]
29#[builder(start_fn = new)]
30pub struct Stack {
31    /// Unique identifier for the stack
32    #[builder(start_fn)]
33    pub id: String,
34    /// Map of resource IDs to their configurations and lifecycle settings
35    #[builder(field)]
36    pub resources: IndexMap<String, ResourceEntry>,
37    /// Combined permissions configuration containing both profiles and management
38    #[builder(field)]
39    #[serde(default)]
40    pub permissions: PermissionsConfig,
41}
42
43impl Stack {
44    /// Returns an iterator over the resources in the stack, including their lifecycle state.
45    pub fn resources(&self) -> impl Iterator<Item = (&String, &ResourceEntry)> {
46        self.resources.iter()
47    }
48
49    /// Returns a mutable iterator over the resources in the stack, including their lifecycle state.
50    pub fn resources_mut(&mut self) -> impl Iterator<Item = (&String, &mut ResourceEntry)> {
51        self.resources.iter_mut()
52    }
53
54    pub fn id(&self) -> &str {
55        &self.id
56    }
57
58    /// Create a reference to the current stack
59    pub fn current() -> StackRef {
60        StackRef::Current
61    }
62
63    /// Returns the permissions configuration for the stack.
64    pub fn permissions(&self) -> &PermissionsConfig {
65        &self.permissions
66    }
67
68    /// Returns the permission profiles for the stack.
69    pub fn permission_profiles(&self) -> &IndexMap<String, PermissionProfile> {
70        &self.permissions.profiles
71    }
72
73    /// Returns the management permissions configuration for the stack.
74    pub fn management(&self) -> &ManagementPermissions {
75        &self.permissions.management
76    }
77}
78
79impl StackBuilder {
80    /// Adds a resource to the stack with its lifecycle state.
81    /// The resource's intrinsic dependencies (from resource.get_dependencies()) are automatically included.
82    /// Use add_with_dependencies() if you need to specify additional dependencies.
83    pub fn add<T: crate::ResourceDefinition>(
84        self,
85        resource: T,
86        lifecycle: ResourceLifecycle,
87    ) -> Self {
88        self.add_with_dependencies(resource, lifecycle, vec![])
89    }
90
91    /// Adds a resource to the stack with its lifecycle state and additional dependencies.
92    /// The total dependencies will be: resource.get_dependencies() + additional_dependencies
93    pub fn add_with_dependencies<T: crate::ResourceDefinition>(
94        mut self,
95        resource: T,
96        lifecycle: ResourceLifecycle,
97        additional_dependencies: Vec<ResourceRef>,
98    ) -> Self {
99        let resource = Resource::new(resource);
100        self.resources.insert(
101            resource.id().to_string(),
102            ResourceEntry {
103                config: resource,
104                lifecycle,
105                dependencies: additional_dependencies,
106                remote_access: false,
107            },
108        );
109        self
110    }
111
112    /// Adds a resource with remote access enabled.
113    /// When remote_access is true, binding params are synced to StackState for external access.
114    pub fn add_with_remote_access<T: crate::ResourceDefinition>(
115        mut self,
116        resource: T,
117        lifecycle: ResourceLifecycle,
118    ) -> Self {
119        let resource = Resource::new(resource);
120        self.resources.insert(
121            resource.id().to_string(),
122            ResourceEntry {
123                config: resource,
124                lifecycle,
125                dependencies: vec![],
126                remote_access: true,
127            },
128        );
129        self
130    }
131
132    /// Sets the permissions configuration for the stack.
133    /// This defines access control for compute services in the stack.
134    pub fn permissions(mut self, permissions: PermissionsConfig) -> Self {
135        self.permissions = permissions;
136        self
137    }
138
139    /// Add a single permission profile to the stack - allows fluent chaining
140    ///
141    /// # Example
142    /// ```rust
143    /// # use alien_core::{Stack, permissions::PermissionProfile};
144    /// Stack::new("my-stack".to_string())
145    ///     .permission("execution", PermissionProfile::new().global(["storage/data-read"]))
146    ///     .permission("management", PermissionProfile::new().global(["storage/management"]))
147    ///     .build()
148    /// # ;
149    /// ```
150    pub fn permission(mut self, name: impl Into<String>, profile: PermissionProfile) -> Self {
151        self.permissions.profiles.insert(name.into(), profile);
152        self
153    }
154
155    /// Sets the management permissions configuration for the stack.
156    /// This defines how management permissions are derived and configured.
157    ///
158    /// # Examples
159    /// ```rust
160    /// # use alien_core::{Stack, permissions::{ManagementPermissions, PermissionProfile}};
161    /// // Auto-derived management permissions (default)
162    /// Stack::new("my-stack".to_string())
163    ///     .management(ManagementPermissions::auto())
164    ///     .build();
165    ///
166    /// // Extend auto-derived permissions
167    /// Stack::new("my-stack".to_string())
168    ///     .management(ManagementPermissions::extend(
169    ///         PermissionProfile::new().global(["vault/data-write"])
170    ///     ))
171    ///     .build();
172    ///
173    /// // Override auto-derived permissions entirely
174    /// Stack::new("my-stack".to_string())
175    ///     .management(ManagementPermissions::override_(
176    ///         PermissionProfile::new().global(["storage/management", "function/management"])
177    ///     ))
178    ///     .build();
179    /// ```
180    pub fn management(mut self, management: ManagementPermissions) -> Self {
181        self.permissions.management = management;
182        self
183    }
184}
185
186/// Reference to a stack for management permissions
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
189#[serde(rename_all = "camelCase")]
190pub enum StackRef {
191    /// Reference to the current stack being built
192    Current,
193    /// Reference to another stack by ID
194    External(String),
195}
196
197impl StackRef {
198    /// Create a StackRef from a stack reference
199    pub fn from_stack(stack: &Stack) -> Self {
200        StackRef::External(stack.id().to_string())
201    }
202}
203
204impl From<&Stack> for StackRef {
205    fn from(stack: &Stack) -> Self {
206        StackRef::External(stack.id().to_string())
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::resource::ResourceLifecycle;
214    use crate::{Function, PermissionSetReference, Storage};
215    use insta::assert_json_snapshot;
216
217    #[test]
218    fn test_stack_serialization() {
219        use crate::FunctionCode;
220
221        let storage = Storage::new("my-bucket".to_string())
222            .public_read(true)
223            .build();
224
225        let function = Function::new("my-function".to_string())
226            .code(FunctionCode::Image {
227                image: "rust:latest".to_string(),
228            })
229            .permissions("execution".to_string())
230            .link(&storage)
231            .build();
232
233        // Create permission profiles for the new system
234        let mut permissions = IndexMap::new();
235        let mut execution_profile = PermissionProfile::new();
236        execution_profile.0.insert(
237            "*".to_string(),
238            vec![
239                PermissionSetReference::from_name("storage/data-read"),
240                PermissionSetReference::from_name("storage/data-write"),
241            ],
242        );
243        permissions.insert("execution".to_string(), execution_profile);
244
245        let stack_builder = Stack::new("test-stack".to_string())
246            .add(storage, ResourceLifecycle::Frozen)
247            .add(function.clone(), ResourceLifecycle::Live);
248
249        let stack = stack_builder
250            .permissions(PermissionsConfig {
251                profiles: permissions,
252                management: ManagementPermissions::Auto,
253            })
254            .build();
255
256        // Serialize and Deserialize
257        let serialized_stack =
258            serde_json::to_string_pretty(&stack).expect("Failed to serialize stack");
259        let deserialized_stack: Stack =
260            serde_json::from_str(&serialized_stack).expect("Failed to deserialize stack");
261
262        // Assert equality
263        assert_eq!(
264            stack, deserialized_stack,
265            "Original and deserialized stacks do not match."
266        );
267
268        // Verify snapshot
269        assert_json_snapshot!("stack_serialization_account_managed", stack);
270    }
271
272    #[test]
273    fn test_empty_stack_serialization() {
274        let stack_builder = Stack::new("empty-test-stack".to_string());
275
276        let stack = stack_builder
277            .permissions(PermissionsConfig::new()) // Empty permissions for existing tests
278            .build();
279
280        // Serialize and Deserialize
281        let serialized_stack =
282            serde_json::to_string_pretty(&stack).expect("Failed to serialize empty stack");
283        let deserialized_stack: Stack =
284            serde_json::from_str(&serialized_stack).expect("Failed to deserialize empty stack");
285
286        // Assert equality
287        assert_eq!(
288            stack, deserialized_stack,
289            "Original and deserialized empty stacks do not match."
290        );
291
292        // Verify snapshot
293        assert_json_snapshot!("empty_stack_serialization_account", stack);
294    }
295
296    #[test]
297    fn test_stack_with_permissions() {
298        use crate::permissions::PermissionProfile;
299        use indexmap::IndexMap;
300
301        // Create a simple stack with permissions
302        let storage = Storage::new("test-storage".to_string()).build();
303
304        // Create a permission profile
305        let mut permission_profile = PermissionProfile::new();
306        permission_profile.0.insert(
307            "*".to_string(),
308            vec![PermissionSetReference::from_name("storage/data-read")],
309        );
310
311        let mut permissions = IndexMap::new();
312        permissions.insert("reader".to_string(), permission_profile);
313
314        let stack = Stack::new("test-permissions-stack".to_string())
315            .add(storage, ResourceLifecycle::Frozen)
316            .permissions(PermissionsConfig {
317                profiles: permissions,
318                management: ManagementPermissions::Auto,
319            })
320            .build();
321
322        // Verify permissions are accessible
323        assert_eq!(stack.permission_profiles().len(), 1);
324        assert!(stack.permission_profiles().contains_key("reader"));
325
326        let reader_profile = stack.permission_profiles().get("reader").unwrap();
327        assert_eq!(reader_profile.0.len(), 1);
328        assert!(reader_profile.0.contains_key("*"));
329
330        let global_permissions = reader_profile.0.get("*").unwrap();
331        assert_eq!(
332            global_permissions,
333            &vec![PermissionSetReference::from_name("storage/data-read")]
334        );
335
336        // Test serialization/deserialization
337        let serialized = serde_json::to_string_pretty(&stack).expect("Failed to serialize");
338        let deserialized: Stack = serde_json::from_str(&serialized).expect("Failed to deserialize");
339        assert_eq!(stack, deserialized);
340    }
341
342    #[test]
343    fn test_stack_with_management_permissions() {
344        use crate::permissions::{ManagementPermissions, PermissionProfile};
345
346        // Create a simple stack with management permissions
347        let storage = Storage::new("test-storage".to_string()).build();
348
349        // Create a permission profile for management
350        let mut management_profile = PermissionProfile::new();
351        management_profile.0.insert(
352            "*".to_string(),
353            vec![PermissionSetReference::from_name("vault/data-write")],
354        );
355
356        // Test auto management permissions (default)
357        let stack_auto = Stack::new("test-auto-management-stack".to_string())
358            .add(storage.clone(), ResourceLifecycle::Frozen)
359            .management(ManagementPermissions::auto())
360            .build();
361
362        assert!(stack_auto.management().is_auto());
363        assert!(stack_auto.management().profile().is_none());
364
365        // Test extend management permissions
366        let stack_extend = Stack::new("test-extend-management-stack".to_string())
367            .add(storage.clone(), ResourceLifecycle::Frozen)
368            .management(ManagementPermissions::extend(management_profile.clone()))
369            .build();
370
371        assert!(stack_extend.management().is_extend());
372        assert_eq!(
373            stack_extend.management().profile().unwrap(),
374            &management_profile
375        );
376
377        // Test override management permissions
378        let stack_override = Stack::new("test-override-management-stack".to_string())
379            .add(storage.clone(), ResourceLifecycle::Frozen)
380            .management(ManagementPermissions::override_(management_profile.clone()))
381            .build();
382
383        assert!(stack_override.management().is_override());
384        assert_eq!(
385            stack_override.management().profile().unwrap(),
386            &management_profile
387        );
388
389        // Test default management permissions
390        let stack_default = Stack::new("test-default-management-stack".to_string())
391            .add(storage, ResourceLifecycle::Frozen)
392            .build();
393
394        assert!(stack_default.management().is_auto());
395
396        // Test serialization/deserialization with management
397        let serialized = serde_json::to_string_pretty(&stack_extend).expect("Failed to serialize");
398        let deserialized: Stack = serde_json::from_str(&serialized).expect("Failed to deserialize");
399        assert_eq!(stack_extend, deserialized);
400    }
401}