1use crate::apigateway::{ApiGatewayV2Api, ApiGatewayV2Integration, ApiGatewayV2Route, ApiGatewayV2Stage};
2use crate::cloudwatch::LogGroup;
3use crate::dynamodb::Table;
4use crate::iam::Role;
5use crate::lambda::{EventSourceMapping, Function, Permission};
6use crate::s3::{Bucket, BucketPolicy};
7use crate::secretsmanager::Secret;
8use crate::shared::Id;
9use crate::sns::{Subscription, Topic};
10use crate::sqs::Queue;
11use rand::Rng;
12use serde::Serialize;
13use std::collections::HashMap;
14use crate::appconfig::{Application, ConfigurationProfile, DeploymentStrategy, Environment};
15use crate::cloudfront::{CachePolicy, Distribution, OriginAccessControl};
16
17#[derive(Debug, Clone)]
18pub struct Asset {
19 pub s3_bucket: String,
20 pub s3_key: String,
21 pub path: String,
22}
23
24#[derive(Debug, Serialize)]
64pub struct Stack {
65 #[serde(skip)]
66 pub(crate) to_replace: Vec<(String, String)>,
67 #[serde(skip)]
68 pub(crate) tags: Vec<(String, String)>,
69 #[serde(rename = "Resources")]
70 pub(crate) resources: HashMap<String, Resource>,
71 #[serde(rename = "Metadata")]
72 pub(crate) metadata: HashMap<String, String>,
73}
74
75impl Stack {
76 pub fn get_tags(&self) -> Vec<(String, String)> {
77 self.tags.clone()
78 }
79
80 pub fn get_assets(&self) -> Vec<Asset> {
81 self.resources
82 .values()
83 .flat_map(|r| match r {
84 Resource::Function(l) => vec![l.asset.clone()], _ => vec![],
86 })
87 .collect()
88 }
89
90 pub fn synth(&self) -> Result<String, String> {
128 let mut naive_synth = serde_json::to_string(self).map_err(|e| format!("Could not serialize stack: {e:#?}"))?;
129 self.to_replace.iter().for_each(|(current, new)| {
131 naive_synth = naive_synth.replace(current, new);
132 });
133
134 Ok(naive_synth)
135 }
136
137 pub fn update_resource_ids_for_existing_stack(&mut self, existing_ids_with_resource_ids: HashMap<String, String>) {
138 let current_ids: HashMap<String, String> = self
139 .resources
140 .iter()
141 .map(|(resource_id, resource)| (resource.get_id().0, resource_id.to_string()))
142 .collect();
143
144 existing_ids_with_resource_ids
145 .into_iter()
146 .filter(|(existing_id, _)| current_ids.contains_key(existing_id))
147 .for_each(|(existing_id, existing_resource_id)| {
148 let current_stack_resource_id = current_ids.get(&existing_id).expect("existence to be checked by filter");
149 let removed = self
150 .resources
151 .remove(current_stack_resource_id)
152 .expect("resource to exist in stack resources");
153 self.resources.insert(existing_resource_id.clone(), removed);
154 self.metadata.insert(existing_id, existing_resource_id.clone());
155 self.to_replace.push((current_stack_resource_id.to_string(), existing_resource_id));
156 });
157 }
158}
159
160#[derive(Debug, Serialize)]
161#[serde(untagged)]
162pub enum Resource {
163 Application(Application),
164 Bucket(Bucket),
165 BucketPolicy(BucketPolicy),
166 ConfigurationProfile(ConfigurationProfile),
167 DeploymentStrategy(DeploymentStrategy),
168 Environment(Environment),
169 Table(Table),
170 Function(Function),
171 LogGroup(LogGroup),
172 Queue(Queue),
173 Topic(Topic),
174 Subscription(Subscription),
175 Permission(Permission),
176 EventSourceMapping(EventSourceMapping),
177 Role(Role),
178 ApiGatewayV2Api(ApiGatewayV2Api),
179 ApiGatewayV2Stage(ApiGatewayV2Stage),
180 ApiGatewayV2Route(ApiGatewayV2Route),
181 ApiGatewayV2Integration(ApiGatewayV2Integration),
182 Secret(Secret),
183 Distribution(Distribution),
184 CachePolicy(CachePolicy),
185 OriginAccessControl(OriginAccessControl),
186}
187
188impl Resource {
189 pub fn get_id(&self) -> Id {
190 let id = match self {
191 Resource::Bucket(r) => r.get_id(),
192 Resource::BucketPolicy(r) => r.get_id(),
193 Resource::Table(r) => r.get_id(),
194 Resource::Function(r) => r.get_id(),
195 Resource::LogGroup(r) => r.get_id(),
196 Resource::Queue(r) => r.get_id(),
197 Resource::Topic(r) => r.get_id(),
198 Resource::Subscription(r) => r.get_id(),
199 Resource::Permission(r) => r.get_id(),
200 Resource::EventSourceMapping(r) => r.get_id(),
201 Resource::Role(r) => r.get_id(),
202 Resource::ApiGatewayV2Api(r) => r.get_id(),
203 Resource::ApiGatewayV2Stage(r) => r.get_id(),
204 Resource::ApiGatewayV2Route(r) => r.get_id(),
205 Resource::ApiGatewayV2Integration(r) => r.get_id(),
206 Resource::Secret(r) => r.get_id(),
207 Resource::Distribution(r) => r.get_id(),
208 Resource::CachePolicy(r) => r.get_id(),
209 Resource::OriginAccessControl(r) => r.get_id(),
210 Resource::Application(r) => r.get_id(),
211 Resource::ConfigurationProfile(r) => r.get_id(),
212 Resource::DeploymentStrategy(r) => r.get_id(),
213 Resource::Environment(r) => r.get_id(),
214 };
215 id.clone()
216 }
217
218 pub fn get_resource_id(&self) -> &str {
219 match self {
220 Resource::Bucket(r) => r.get_resource_id(),
221 Resource::BucketPolicy(r) => r.get_resource_id(),
222 Resource::Table(t) => t.get_resource_id(),
223 Resource::Function(r) => r.get_resource_id(),
224 Resource::Role(r) => r.get_resource_id(),
225 Resource::Queue(r) => r.get_resource_id(),
226 Resource::EventSourceMapping(r) => r.get_resource_id(),
227 Resource::LogGroup(r) => r.get_resource_id(),
228 Resource::Topic(r) => r.get_resource_id(),
229 Resource::Subscription(r) => r.get_resource_id(),
230 Resource::Permission(r) => r.get_resource_id(),
231 Resource::ApiGatewayV2Api(r) => r.get_resource_id(),
232 Resource::ApiGatewayV2Stage(r) => r.get_resource_id(),
233 Resource::ApiGatewayV2Route(r) => r.get_resource_id(),
234 Resource::ApiGatewayV2Integration(r) => r.get_resource_id(),
235 Resource::Secret(r) => r.get_resource_id(),
236 Resource::Distribution(r) => r.get_resource_id(),
237 Resource::CachePolicy(r) => r.get_resource_id(),
238 Resource::OriginAccessControl(r) => r.get_resource_id(),
239 Resource::Application(r) => r.get_resource_id(),
240 Resource::ConfigurationProfile(r) => r.get_resource_id(),
241 Resource::DeploymentStrategy(r) => r.get_resource_id(),
242 Resource::Environment(r) => r.get_resource_id(),
243 }
244 }
245
246 pub(crate) fn generate_id(resource_name: &str) -> String {
247 let mut rng = rand::rng();
248 let random_suffix: u32 = rng.random();
249 format!("{resource_name}{random_suffix}")
250 }
251}
252
253macro_rules! from_resource {
254 ($name:ident) => {
255 impl From<$name> for Resource {
256 fn from(value: $name) -> Self {
257 Resource::$name(value)
258 }
259 }
260 };
261}
262
263from_resource!(Application);
264from_resource!(Bucket);
265from_resource!(BucketPolicy);
266from_resource!(ConfigurationProfile);
267from_resource!(DeploymentStrategy);
268from_resource!(Environment);
269from_resource!(Table);
270from_resource!(Function);
271from_resource!(Role);
272from_resource!(LogGroup);
273from_resource!(Queue);
274from_resource!(Topic);
275from_resource!(EventSourceMapping);
276from_resource!(Permission);
277from_resource!(Subscription);
278from_resource!(ApiGatewayV2Api);
279from_resource!(ApiGatewayV2Stage);
280from_resource!(ApiGatewayV2Route);
281from_resource!(ApiGatewayV2Integration);
282from_resource!(Secret);
283from_resource!(Distribution);
284from_resource!(CachePolicy);
285from_resource!(OriginAccessControl);
286
287#[cfg(test)]
288mod tests {
289 use crate::sns::TopicBuilder;
290 use crate::sqs::QueueBuilder;
291 use crate::stack::StackBuilder;
292 use std::collections::HashMap;
293
294 #[test]
295 fn should_do_nothing_for_empty_stack_and_empty_existing_ids() {
296 let mut stack_builder = StackBuilder::new().build().unwrap();
297 let existing_ids = HashMap::new();
298
299 stack_builder.update_resource_ids_for_existing_stack(existing_ids);
300
301 assert_eq!(stack_builder.resources.len(), 0);
302 assert_eq!(stack_builder.metadata.len(), 0);
303 assert_eq!(stack_builder.to_replace.len(), 0);
304 }
305
306 #[test]
307 fn should_do_nothing_for_empty_stack() {
308 let mut stack_builder = StackBuilder::new().build().unwrap();
309 let mut existing_ids = HashMap::new();
310 existing_ids.insert("fun".to_string(), "abc123".to_string());
311
312 stack_builder.update_resource_ids_for_existing_stack(existing_ids);
313
314 assert_eq!(stack_builder.resources.len(), 0);
315 assert_eq!(stack_builder.metadata.len(), 0);
316 assert_eq!(stack_builder.to_replace.len(), 0);
317 }
318
319 #[test]
320 fn should_replace_topic_resource_id_with_the_existing_id() {
321 let mut stack_builder = StackBuilder::new();
322 TopicBuilder::new("topic").build(&mut stack_builder);
323 let mut existing_ids = HashMap::new();
324 existing_ids.insert("topic".to_string(), "abc123".to_string());
325 let mut stack = stack_builder.build().unwrap();
326
327 stack.update_resource_ids_for_existing_stack(existing_ids);
328
329 assert_eq!(stack.resources.len(), 1);
330 assert_eq!(stack.to_replace.len(), 1);
331 assert_eq!(stack.metadata.len(), 1);
332 assert_eq!(stack.metadata.get("topic").unwrap(), &"abc123".to_string());
333 }
334
335 #[test]
336 fn should_replace_topic_resource_id_with_the_existing_id_keeping_new_queue_id() {
337 let mut stack_builder = StackBuilder::new();
338 TopicBuilder::new("topic").build(&mut stack_builder);
339 QueueBuilder::new("queue").standard_queue().build(&mut stack_builder);
340 let mut existing_ids = HashMap::new();
341 existing_ids.insert("topic".to_string(), "abc123".to_string());
342 let mut stack = stack_builder.build().unwrap();
343
344 stack.update_resource_ids_for_existing_stack(existing_ids);
345
346 assert_eq!(stack.resources.len(), 2);
347 assert_eq!(stack.to_replace.len(), 1);
348 assert_eq!(stack.metadata.len(), 2);
349 assert_eq!(stack.metadata.get("topic").unwrap(), &"abc123".to_string());
350 }
351}