rusty_cdk_core/stack/
builder.rs

1use crate::stack::{Resource, Stack};
2use std::error::Error;
3use std::fmt::{Debug, Display, Formatter};
4use crate::shared::Id;
5
6#[derive(Debug)]
7pub enum StackBuilderError {
8    MissingPermissionsForRole(Vec<String>),
9    DuplicateIds(Vec<String>),
10    DuplicateResourceIds(Vec<String>),
11}
12
13impl Display for StackBuilderError {
14    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
15        match self {
16            StackBuilderError::MissingPermissionsForRole(info) => {
17                let gathered_info = info.join(";");
18                f.write_fmt(format_args!(
19                    "one or more roles seem to be missing permission to access services: `{}`?",
20                    gathered_info
21                ))
22            }
23            StackBuilderError::DuplicateIds(info) => {
24                let gathered_info = info.join(";");
25                f.write_fmt(format_args!(
26                    "ids should be unique, but the following duplicates were detected: `{}`",
27                    gathered_info
28                ))
29            }
30            StackBuilderError::DuplicateResourceIds(info) => {
31                let gathered_info = info.join(";");
32                f.write_fmt(format_args!(
33                    "duplicate resource ids were detected (`{}`), rerunning this command should generate new ones",
34                    gathered_info
35                ))
36            }
37        }
38    }
39}
40
41impl Error for StackBuilderError {}
42
43/// Builder for CloudFormation stacks.
44///
45/// Collects resources and manages their relationships.
46/// Might validate whether IAM roles are missing permissions for AWS services they need to access, based on Cargo.toml dependencies.
47///
48/// # Example
49///
50/// ```rust
51/// use rusty_cdk_core::stack::StackBuilder;
52/// use rusty_cdk_core::sqs::QueueBuilder;
53/// use rusty_cdk_core::wrappers::*;
54///
55/// let mut stack_builder = StackBuilder::new();
56///
57/// // Add resources to the stack
58/// let queue = QueueBuilder::new("my-queue")
59///     .standard_queue()
60///     .build(&mut stack_builder);
61///
62/// // Add tags to the stack
63/// stack_builder = stack_builder
64///     .add_tag("Environment", "Production")
65///     .add_tag("Owner", "Team");
66///
67/// // Build the stack
68/// let stack = stack_builder.build().expect("Stack to build successfully");
69/// ```
70pub struct StackBuilder {
71    resources: Vec<Resource>,
72    tags: Vec<(String, String)>,
73}
74
75impl Default for StackBuilder {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl StackBuilder {
82    pub fn new() -> Self {
83        Self {
84            resources: vec![],
85            tags: vec![],
86        }
87    }
88
89    pub fn add_resource<T: Into<Resource>>(&mut self, resource: T) {
90        let resource = resource.into();
91        self.resources.push(resource);
92    }
93
94    pub fn add_tag<T: Into<String>>(mut self, key: T, value: T) -> Self {
95        self.tags.push((key.into(), value.into()));
96        self
97    }
98    
99    pub(crate) fn get_resource(&mut self, id: &Id) -> Option<&mut Resource> {
100        self.resources.iter_mut()
101            .find(|v| &v.get_id() == id)
102    }
103
104    /// Builds the stack and validates all resources.
105    ///
106    /// Might return an error if any IAM roles are missing permissions for AWS services they need to access, based on Cargo.toml dependencies.
107    pub fn build(self) -> Result<Stack, StackBuilderError> {
108        let (ids, resource_ids) = self.resources
109            .iter()
110            .map(|r| (r.get_id().to_string(), r.get_resource_id().to_string()))
111            .collect::<(Vec<_>, Vec<_>)>();
112        
113        let duplicate_ids = Self::check_for_duplicate_ids(ids);
114        let resource_ids = Self::check_for_duplicate_ids(resource_ids);
115        
116        if !duplicate_ids.is_empty() {
117            return Err(StackBuilderError::DuplicateIds(
118                duplicate_ids,
119            ));
120        }
121        if !resource_ids.is_empty() {
122            return Err(StackBuilderError::DuplicateResourceIds(
123                resource_ids,
124            ));
125        }
126        
127        let roles_with_potentially_missing_services: Vec<_> = self.check_for_roles_with_missing_permissions();
128
129        if !roles_with_potentially_missing_services.is_empty() {
130            return Err(StackBuilderError::MissingPermissionsForRole(
131                roles_with_potentially_missing_services,
132            ));
133        }
134        
135
136        let metadata = self
137            .resources
138            .iter()
139            .map(|r| (r.get_id().to_string(), r.get_resource_id().to_string()))
140            .collect();
141
142
143
144        let resources = self.resources.into_iter().map(|r| (r.get_resource_id().to_string(), r)).collect();
145        Ok(Stack {
146            resource_ids_to_replace: vec![],
147            tags: self.tags,
148            resources,
149            metadata,
150        })
151    }
152
153    fn check_for_roles_with_missing_permissions(&self) -> Vec<String> {
154        self.resources
155            .iter()
156            .filter_map(|r| match r {
157                Resource::Role(r) => {
158                    if !r.potentially_missing_services.is_empty() {
159                        Some(format!("{}: {}", r.resource_id, r.potentially_missing_services.join(",")))
160                    } else {
161                        None
162                    }
163                }
164                _ => None,
165            })
166            .collect()
167    }
168
169    fn check_for_duplicate_ids(ids: Vec<String>) -> Vec<String> {
170        let results = ids.into_iter().fold((vec![], vec![]), |(mut all, mut duplicates), curr| {
171            if all.contains(&curr) && !duplicates.contains(&curr) {
172                duplicates.push(curr.clone());
173            }
174            all.push(curr);
175            (all, duplicates)
176        });
177        results.1
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use crate::stack::StackBuilder;
184
185    #[test]
186    fn test_check_for_duplicate_ids() {
187        let duplicates = StackBuilder::check_for_duplicate_ids(vec!["bucket".to_string(), "bucket".to_string(), "topic".to_string(), "queue".to_string(), "bucket".to_string(), "table".to_string(), "topic".to_string()]);
188        
189        assert_eq!(duplicates, vec!["bucket", "topic"])
190    }
191}