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 (sort maps to be deterministic across Rust versions)
269        let mut settings = insta::Settings::clone_current();
270        settings.set_sort_maps(true);
271        settings.bind(|| {
272            assert_json_snapshot!("stack_serialization_account_managed", stack);
273        });
274    }
275
276    #[test]
277    fn test_empty_stack_serialization() {
278        let stack_builder = Stack::new("empty-test-stack".to_string());
279
280        let stack = stack_builder
281            .permissions(PermissionsConfig::new()) // Empty permissions for existing tests
282            .build();
283
284        // Serialize and Deserialize
285        let serialized_stack =
286            serde_json::to_string_pretty(&stack).expect("Failed to serialize empty stack");
287        let deserialized_stack: Stack =
288            serde_json::from_str(&serialized_stack).expect("Failed to deserialize empty stack");
289
290        // Assert equality
291        assert_eq!(
292            stack, deserialized_stack,
293            "Original and deserialized empty stacks do not match."
294        );
295
296        // Verify snapshot (sort maps to be deterministic across Rust versions)
297        let mut settings = insta::Settings::clone_current();
298        settings.set_sort_maps(true);
299        settings.bind(|| {
300            assert_json_snapshot!("empty_stack_serialization_account", stack);
301        });
302    }
303
304    #[test]
305    fn test_stack_with_permissions() {
306        use crate::permissions::PermissionProfile;
307        use indexmap::IndexMap;
308
309        // Create a simple stack with permissions
310        let storage = Storage::new("test-storage".to_string()).build();
311
312        // Create a permission profile
313        let mut permission_profile = PermissionProfile::new();
314        permission_profile.0.insert(
315            "*".to_string(),
316            vec![PermissionSetReference::from_name("storage/data-read")],
317        );
318
319        let mut permissions = IndexMap::new();
320        permissions.insert("reader".to_string(), permission_profile);
321
322        let stack = Stack::new("test-permissions-stack".to_string())
323            .add(storage, ResourceLifecycle::Frozen)
324            .permissions(PermissionsConfig {
325                profiles: permissions,
326                management: ManagementPermissions::Auto,
327            })
328            .build();
329
330        // Verify permissions are accessible
331        assert_eq!(stack.permission_profiles().len(), 1);
332        assert!(stack.permission_profiles().contains_key("reader"));
333
334        let reader_profile = stack.permission_profiles().get("reader").unwrap();
335        assert_eq!(reader_profile.0.len(), 1);
336        assert!(reader_profile.0.contains_key("*"));
337
338        let global_permissions = reader_profile.0.get("*").unwrap();
339        assert_eq!(
340            global_permissions,
341            &vec![PermissionSetReference::from_name("storage/data-read")]
342        );
343
344        // Test serialization/deserialization
345        let serialized = serde_json::to_string_pretty(&stack).expect("Failed to serialize");
346        let deserialized: Stack = serde_json::from_str(&serialized).expect("Failed to deserialize");
347        assert_eq!(stack, deserialized);
348    }
349
350    #[test]
351    fn test_stack_with_management_permissions() {
352        use crate::permissions::{ManagementPermissions, PermissionProfile};
353
354        // Create a simple stack with management permissions
355        let storage = Storage::new("test-storage".to_string()).build();
356
357        // Create a permission profile for management
358        let mut management_profile = PermissionProfile::new();
359        management_profile.0.insert(
360            "*".to_string(),
361            vec![PermissionSetReference::from_name("vault/data-write")],
362        );
363
364        // Test auto management permissions (default)
365        let stack_auto = Stack::new("test-auto-management-stack".to_string())
366            .add(storage.clone(), ResourceLifecycle::Frozen)
367            .management(ManagementPermissions::auto())
368            .build();
369
370        assert!(stack_auto.management().is_auto());
371        assert!(stack_auto.management().profile().is_none());
372
373        // Test extend management permissions
374        let stack_extend = Stack::new("test-extend-management-stack".to_string())
375            .add(storage.clone(), ResourceLifecycle::Frozen)
376            .management(ManagementPermissions::extend(management_profile.clone()))
377            .build();
378
379        assert!(stack_extend.management().is_extend());
380        assert_eq!(
381            stack_extend.management().profile().unwrap(),
382            &management_profile
383        );
384
385        // Test override management permissions
386        let stack_override = Stack::new("test-override-management-stack".to_string())
387            .add(storage.clone(), ResourceLifecycle::Frozen)
388            .management(ManagementPermissions::override_(management_profile.clone()))
389            .build();
390
391        assert!(stack_override.management().is_override());
392        assert_eq!(
393            stack_override.management().profile().unwrap(),
394            &management_profile
395        );
396
397        // Test default management permissions
398        let stack_default = Stack::new("test-default-management-stack".to_string())
399            .add(storage, ResourceLifecycle::Frozen)
400            .build();
401
402        assert!(stack_default.management().is_auto());
403
404        // Test serialization/deserialization with management
405        let serialized = serde_json::to_string_pretty(&stack_extend).expect("Failed to serialize");
406        let deserialized: Stack = serde_json::from_str(&serialized).expect("Failed to deserialize");
407        assert_eq!(stack_extend, deserialized);
408    }
409}