Skip to main content

rusty_cdk_core/stack/
builder.rs

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