Skip to main content

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:
107    /// - there are duplicate ids
108    /// - IAM roles are missing permissions for AWS services they need to access (only when Cargo.toml dependencies were passed in)
109    pub fn build(self) -> Result<Stack, StackBuilderError> {
110        let (ids, resource_ids) = self.resources
111            .iter()
112            .map(|r| (r.get_id().to_string(), r.get_resource_id().to_string()))
113            .collect::<(Vec<_>, Vec<_>)>();
114        
115        let duplicate_ids = Self::check_for_duplicate_ids(ids);
116        let resource_ids = Self::check_for_duplicate_ids(resource_ids);
117        
118        if !duplicate_ids.is_empty() {
119            return Err(StackBuilderError::DuplicateIds(
120                duplicate_ids,
121            ));
122        }
123        if !resource_ids.is_empty() {
124            return Err(StackBuilderError::DuplicateResourceIds(
125                resource_ids,
126            ));
127        }
128        
129        let roles_with_potentially_missing_services: Vec<_> = self.check_for_roles_with_missing_permissions();
130
131        if !roles_with_potentially_missing_services.is_empty() {
132            return Err(StackBuilderError::MissingPermissionsForRole(
133                roles_with_potentially_missing_services,
134            ));
135        }
136
137        let metadata = self
138            .resources
139            .iter()
140            .map(|r| (r.get_id().to_string(), r.get_resource_id().to_string()))
141            .collect();
142
143        let resources = self.resources.into_iter().map(|r| (r.get_resource_id().to_string(), r)).collect();
144        Ok(Stack {
145            resource_ids_to_replace: vec![],
146            tags: self.tags,
147            resources,
148            metadata,
149        })
150    }
151
152    fn check_for_roles_with_missing_permissions(&self) -> Vec<String> {
153        self.resources
154            .iter()
155            .filter_map(|r| match r {
156                Resource::Role(r) => {
157                    if !r.potentially_missing_services.is_empty() {
158                        Some(format!("{}: {}", r.resource_id, r.potentially_missing_services.join(",")))
159                    } else {
160                        None
161                    }
162                }
163                _ => None,
164            })
165            .collect()
166    }
167
168    fn check_for_duplicate_ids(ids: Vec<String>) -> Vec<String> {
169        let results = ids.into_iter().fold((vec![], vec![]), |(mut all, mut duplicates), curr| {
170            if all.contains(&curr) && !duplicates.contains(&curr) {
171                duplicates.push(curr.clone());
172            }
173            all.push(curr);
174            (all, duplicates)
175        });
176        results.1
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use crate::stack::StackBuilder;
183
184    #[test]
185    fn test_check_for_duplicate_ids() {
186        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()]);
187        
188        assert_eq!(duplicates, vec!["bucket", "topic"])
189    }
190}