rusty_cdk_core/stack/
builder.rs

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