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, TopicPolicy};
10use crate::sqs::{Queue, QueuePolicy};
11use rand::Rng;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::fmt::{Display, Formatter};
15use crate::appconfig::{Application, ConfigurationProfile, DeploymentStrategy, Environment};
16use crate::cloudfront::{CachePolicy, Distribution, OriginAccessControl};
17use crate::custom_resource::BucketNotification;
18
19#[derive(Debug, Clone)]
20pub struct Asset {
21 pub s3_bucket: String,
22 pub s3_key: String,
23 pub path: String,
24}
25
26impl Display for Asset {
27 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
28 f.write_fmt(format_args!("Asset at path {} for bucket {} and key {}", self.path, self.s3_bucket, self.s3_key))
29 }
30}
31
32#[derive(Debug, Serialize)]
72pub struct Stack {
73 #[serde(skip)]
74 pub(crate) to_replace: Vec<(String, String)>,
75 #[serde(skip)]
76 pub(crate) tags: Vec<(String, String)>,
77 #[serde(rename = "Resources")]
78 pub(crate) resources: HashMap<String, Resource>,
79 #[serde(rename = "Metadata")]
80 pub(crate) metadata: HashMap<String, String>,
81}
82
83#[derive(Debug, Deserialize)]
84struct StackOnlyMetadata {
85 #[serde(rename = "Metadata")]
86 pub(crate) metadata: HashMap<String, String>,
87}
88
89impl Stack {
90 pub fn get_tags(&self) -> Vec<(String, String)> {
91 self.tags.clone()
92 }
93
94 pub fn get_assets(&self) -> Vec<Asset> {
95 self.resources
96 .values()
97 .flat_map(|r| match r {
98 Resource::Function(l) => l.asset.clone(), _ => None,
100 })
101 .collect()
102 }
103
104 pub fn synth(&self) -> Result<String, String> {
146 let mut naive_synth = serde_json::to_string(self).map_err(|e| format!("Could not serialize stack: {e:#?}"))?;
147 self.to_replace.iter().for_each(|(current, new)| {
149 naive_synth = naive_synth.replace(current, new);
150 });
151
152 Ok(naive_synth)
153 }
154
155 pub fn synth_for_existing(&mut self, existing_stack: &str) -> Result<String, String> {
206 let meta: StackOnlyMetadata = serde_json::from_str(existing_stack).map_err(|_| {
207 "Could not retrieve resource info from existing stack".to_string()
208 })?;
209 self.update_resource_ids_for_existing_stack(meta.metadata);
210 self.synth()
211 }
212
213 fn update_resource_ids_for_existing_stack(&mut self, existing_ids_with_resource_ids: HashMap<String, String>) {
214 let current_ids: HashMap<String, String> = self
215 .resources
216 .iter()
217 .map(|(resource_id, resource)| (resource.get_id().0, resource_id.to_string()))
218 .collect();
219
220 existing_ids_with_resource_ids
221 .into_iter()
222 .filter(|(existing_id, _)| current_ids.contains_key(existing_id))
223 .for_each(|(existing_id, existing_resource_id)| {
224 let current_stack_resource_id = current_ids.get(&existing_id).expect("existence to be checked by filter");
225 let removed = self
226 .resources
227 .remove(current_stack_resource_id)
228 .expect("resource to exist in stack resources");
229 self.resources.insert(existing_resource_id.clone(), removed);
230 self.metadata.insert(existing_id, existing_resource_id.clone());
231 self.to_replace.push((current_stack_resource_id.to_string(), existing_resource_id));
232 });
233 }
234}
235
236#[derive(Debug, Serialize)]
237#[serde(untagged)]
238pub enum Resource {
239 Application(Application),
240 Bucket(Bucket),
241 BucketPolicy(BucketPolicy),
242 BucketNotification(BucketNotification),
243 ConfigurationProfile(ConfigurationProfile),
244 DeploymentStrategy(DeploymentStrategy),
245 Environment(Environment),
246 Table(Table),
247 Function(Function),
248 LogGroup(LogGroup),
249 Queue(Queue),
250 QueuePolicy(QueuePolicy),
251 Topic(Topic),
252 TopicPolicy(TopicPolicy),
253 Subscription(Subscription),
254 Permission(Permission),
255 EventSourceMapping(EventSourceMapping),
256 Role(Role),
257 ApiGatewayV2Api(ApiGatewayV2Api),
258 ApiGatewayV2Stage(ApiGatewayV2Stage),
259 ApiGatewayV2Route(ApiGatewayV2Route),
260 ApiGatewayV2Integration(ApiGatewayV2Integration),
261 Secret(Secret),
262 Distribution(Distribution),
263 CachePolicy(CachePolicy),
264 OriginAccessControl(OriginAccessControl),
265}
266
267impl Resource {
268 pub fn get_id(&self) -> Id {
269 let id = match self {
270 Resource::Bucket(r) => r.get_id(),
271 Resource::BucketPolicy(r) => r.get_id(),
272 Resource::Table(r) => r.get_id(),
273 Resource::Function(r) => r.get_id(),
274 Resource::LogGroup(r) => r.get_id(),
275 Resource::Queue(r) => r.get_id(),
276 Resource::Topic(r) => r.get_id(),
277 Resource::Subscription(r) => r.get_id(),
278 Resource::Permission(r) => r.get_id(),
279 Resource::EventSourceMapping(r) => r.get_id(),
280 Resource::Role(r) => r.get_id(),
281 Resource::ApiGatewayV2Api(r) => r.get_id(),
282 Resource::ApiGatewayV2Stage(r) => r.get_id(),
283 Resource::ApiGatewayV2Route(r) => r.get_id(),
284 Resource::ApiGatewayV2Integration(r) => r.get_id(),
285 Resource::Secret(r) => r.get_id(),
286 Resource::Distribution(r) => r.get_id(),
287 Resource::CachePolicy(r) => r.get_id(),
288 Resource::OriginAccessControl(r) => r.get_id(),
289 Resource::Application(r) => r.get_id(),
290 Resource::ConfigurationProfile(r) => r.get_id(),
291 Resource::DeploymentStrategy(r) => r.get_id(),
292 Resource::Environment(r) => r.get_id(),
293 Resource::BucketNotification(r) => r.get_id(),
294 Resource::TopicPolicy(r) => r.get_id(),
295 Resource::QueuePolicy(r) => r.get_id(),
296 };
297 id.clone()
298 }
299
300 pub fn get_resource_id(&self) -> &str {
301 match self {
302 Resource::Bucket(r) => r.get_resource_id(),
303 Resource::BucketPolicy(r) => r.get_resource_id(),
304 Resource::Table(t) => t.get_resource_id(),
305 Resource::Function(r) => r.get_resource_id(),
306 Resource::Role(r) => r.get_resource_id(),
307 Resource::Queue(r) => r.get_resource_id(),
308 Resource::EventSourceMapping(r) => r.get_resource_id(),
309 Resource::LogGroup(r) => r.get_resource_id(),
310 Resource::Topic(r) => r.get_resource_id(),
311 Resource::Subscription(r) => r.get_resource_id(),
312 Resource::Permission(r) => r.get_resource_id(),
313 Resource::ApiGatewayV2Api(r) => r.get_resource_id(),
314 Resource::ApiGatewayV2Stage(r) => r.get_resource_id(),
315 Resource::ApiGatewayV2Route(r) => r.get_resource_id(),
316 Resource::ApiGatewayV2Integration(r) => r.get_resource_id(),
317 Resource::Secret(r) => r.get_resource_id(),
318 Resource::Distribution(r) => r.get_resource_id(),
319 Resource::CachePolicy(r) => r.get_resource_id(),
320 Resource::OriginAccessControl(r) => r.get_resource_id(),
321 Resource::Application(r) => r.get_resource_id(),
322 Resource::ConfigurationProfile(r) => r.get_resource_id(),
323 Resource::DeploymentStrategy(r) => r.get_resource_id(),
324 Resource::Environment(r) => r.get_resource_id(),
325 Resource::BucketNotification(r) => r.get_resource_id(),
326 Resource::TopicPolicy(r) => r.get_resource_id(),
327 Resource::QueuePolicy(r) => r.get_resource_id(),
328 }
329 }
330
331 pub(crate) fn generate_id(resource_name: &str) -> String {
332 let mut rng = rand::rng();
333 let random_suffix: u32 = rng.random();
334 format!("{resource_name}{random_suffix}")
335 }
336}
337
338macro_rules! from_resource {
339 ($name:ident) => {
340 impl From<$name> for Resource {
341 fn from(value: $name) -> Self {
342 Resource::$name(value)
343 }
344 }
345 };
346}
347
348from_resource!(Application);
349from_resource!(Bucket);
350from_resource!(BucketPolicy);
351from_resource!(ConfigurationProfile);
352from_resource!(DeploymentStrategy);
353from_resource!(Environment);
354from_resource!(Table);
355from_resource!(Function);
356from_resource!(Role);
357from_resource!(LogGroup);
358from_resource!(Queue);
359from_resource!(QueuePolicy);
360from_resource!(Topic);
361from_resource!(TopicPolicy);
362from_resource!(EventSourceMapping);
363from_resource!(Permission);
364from_resource!(Subscription);
365from_resource!(ApiGatewayV2Api);
366from_resource!(ApiGatewayV2Stage);
367from_resource!(ApiGatewayV2Route);
368from_resource!(ApiGatewayV2Integration);
369from_resource!(Secret);
370from_resource!(Distribution);
371from_resource!(CachePolicy);
372from_resource!(OriginAccessControl);
373from_resource!(BucketNotification);
374
375#[cfg(test)]
376mod tests {
377 use crate::sns::TopicBuilder;
378 use crate::sqs::QueueBuilder;
379 use crate::stack::StackBuilder;
380 use std::collections::HashMap;
381
382 #[test]
383 fn should_do_nothing_for_empty_stack_and_empty_existing_ids() {
384 let mut stack_builder = StackBuilder::new().build().unwrap();
385 let existing_ids = HashMap::new();
386
387 stack_builder.update_resource_ids_for_existing_stack(existing_ids);
388
389 assert_eq!(stack_builder.resources.len(), 0);
390 assert_eq!(stack_builder.metadata.len(), 0);
391 assert_eq!(stack_builder.to_replace.len(), 0);
392 }
393
394 #[test]
395 fn should_do_nothing_for_empty_stack() {
396 let mut stack_builder = StackBuilder::new().build().unwrap();
397 let mut existing_ids = HashMap::new();
398 existing_ids.insert("fun".to_string(), "abc123".to_string());
399
400 stack_builder.update_resource_ids_for_existing_stack(existing_ids);
401
402 assert_eq!(stack_builder.resources.len(), 0);
403 assert_eq!(stack_builder.metadata.len(), 0);
404 assert_eq!(stack_builder.to_replace.len(), 0);
405 }
406
407 #[test]
408 fn should_replace_topic_resource_id_with_the_existing_id() {
409 let mut stack_builder = StackBuilder::new();
410 TopicBuilder::new("topic").build(&mut stack_builder);
411 let mut existing_ids = HashMap::new();
412 existing_ids.insert("topic".to_string(), "abc123".to_string());
413 let mut stack = stack_builder.build().unwrap();
414
415 stack.update_resource_ids_for_existing_stack(existing_ids);
416
417 assert_eq!(stack.resources.len(), 1);
418 assert_eq!(stack.to_replace.len(), 1);
419 assert_eq!(stack.metadata.len(), 1);
420 assert_eq!(stack.metadata.get("topic").unwrap(), &"abc123".to_string());
421 }
422
423 #[test]
424 fn should_replace_topic_resource_id_with_the_existing_id_keeping_new_queue_id() {
425 let mut stack_builder = StackBuilder::new();
426 TopicBuilder::new("topic").build(&mut stack_builder);
427 QueueBuilder::new("queue").standard_queue().build(&mut stack_builder);
428 let mut existing_ids = HashMap::new();
429 existing_ids.insert("topic".to_string(), "abc123".to_string());
430 let mut stack = stack_builder.build().unwrap();
431
432 stack.update_resource_ids_for_existing_stack(existing_ids);
433
434 assert_eq!(stack.resources.len(), 2);
435 assert_eq!(stack.to_replace.len(), 1);
436 assert_eq!(stack.metadata.len(), 2);
437 assert_eq!(stack.metadata.get("topic").unwrap(), &"abc123".to_string());
438 }
439}