1use crate::apigateway::{ApiGatewayV2Api, ApiGatewayV2Integration, ApiGatewayV2Route, ApiGatewayV2Stage};
2use crate::appconfig::{Application, ConfigurationProfile, DeploymentStrategy, Environment};
3use crate::appsync::{AppSyncApi, ChannelNamespace};
4use crate::cloudfront::{CachePolicy, Distribution, OriginAccessControl};
5use crate::cloudwatch::LogGroup;
6use crate::custom_resource::BucketNotification;
7use crate::dynamodb::Table;
8use crate::events::Schedule;
9use crate::iam::Role;
10use crate::lambda::{EventSourceMapping, Function, Permission};
11use crate::s3::{Bucket, BucketPolicy};
12use crate::secretsmanager::Secret;
13use crate::shared::{DeletionPolicy, Id};
14use crate::sns::{Subscription, Topic, TopicPolicy};
15use crate::sqs::{Queue, QueuePolicy};
16use rand::Rng;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use std::fmt::{Display, Formatter};
20
21#[derive(Debug)]
22pub enum Cleanable<'a> {
23 Bucket(&'a str),
24 Topic(&'a str),
25}
26
27#[derive(Debug)]
28pub struct StackDiff {
29 pub unchanged_ids: Vec<(String, String)>,
31 pub ids_to_be_removed: Vec<(String, String)>,
32 pub new_ids: Vec<(String, String)>,
33}
34
35#[derive(Debug, Clone)]
36pub struct Asset {
37 pub s3_bucket: String,
38 pub s3_key: String,
39 pub path: String,
40}
41
42impl Display for Asset {
43 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
44 f.write_fmt(format_args!(
45 "Asset at path {} for bucket {} and key {}",
46 self.path, self.s3_bucket, self.s3_key
47 ))
48 }
49}
50
51#[derive(Debug, Serialize, Deserialize)]
91pub struct Stack {
92 #[serde(skip)]
93 pub(crate) resource_ids_to_replace: Vec<(String, String)>,
94 #[serde(skip)]
95 pub(crate) tags: Vec<(String, String)>,
96 #[serde(rename = "Resources")]
97 pub(crate) resources: HashMap<String, Resource>,
98 #[serde(rename = "Metadata")]
99 pub(crate) metadata: HashMap<String, String>,
100}
101
102#[derive(Debug, Deserialize)]
103struct StackOnlyMetadata {
104 #[serde(rename = "Metadata")]
105 pub(crate) metadata: HashMap<String, String>,
106}
107
108impl Stack {
109 pub fn get_tags(&self) -> Vec<(String, String)> {
110 self.tags.clone()
111 }
112
113 pub fn get_assets(&self) -> Vec<Asset> {
114 self.resources
115 .values()
116 .flat_map(|r| match r {
117 Resource::Function(l) => l.asset.clone(), _ => None,
119 })
120 .collect()
121 }
122
123 pub fn synth(&self) -> Result<String, String> {
165 let mut naive_synth = serde_json::to_string(self).map_err(|e| format!("Could not serialize stack: {e:#?}"))?;
166 self.resource_ids_to_replace.iter().for_each(|(current, new)| {
168 naive_synth = naive_synth.replace(current, new);
169 });
170
171 Ok(naive_synth)
172 }
173
174 pub fn synth_for_existing(&mut self, existing_stack: &str) -> Result<String, String> {
223 let meta = Self::get_metadata(existing_stack)?;
224 self.update_resource_ids_for_existing_stack(meta.metadata);
225 self.synth()
226 }
227
228 pub fn get_cleanable_resources(&'_ self) -> Vec<Cleanable<'_>> {
229 self.resources.iter().flat_map(|(k, r)| {
230 match r {
231 Resource::Bucket(b) => {
232 if let Some(pol) = &b.update_delete_policy_dto.deletion_policy {
233 let pol: DeletionPolicy = pol.into();
234 match pol {
235 DeletionPolicy::Retain => None,
236 _ => Some(Cleanable::Bucket(k))
237 }
238 } else {
239 Some(Cleanable::Bucket(k))
240 }
241 }
242 Resource::Topic(t) => {
243 if t.properties.archive_policy.is_some() {
245 Some(Cleanable::Topic(k))
246 } else {
247 None
248 }
249 }
250 _ => None
251 }
252 }).collect()
253 }
254
255 pub fn get_diff(&self, existing_stack: &str) -> Result<StackDiff, String> {
256 let meta = Self::get_metadata(existing_stack)?;
257 let existing_meta = meta.metadata;
258 let existing_ids: Vec<_> = existing_meta.keys().cloned().collect();
259
260 let new_meta = &self.metadata;
261 let new_ids: Vec<_> = new_meta.keys().cloned().collect();
262
263 let (in_existing, not_in_existing): (Vec<_>, Vec<_>) = new_ids.into_iter().partition(|v| existing_ids.contains(v));
264 let removed: Vec<_> = existing_ids.into_iter().filter(|v| !in_existing.contains(v)).collect();
265
266 let in_existing = in_existing
267 .into_iter()
268 .map(|v| {
269 let resource_id = existing_meta.get(&v).expect("resource id to be present").to_string();
270 (v, resource_id)
271 })
272 .collect();
273 let removed = removed
274 .into_iter()
275 .map(|v| {
276 let resource_id = existing_meta.get(&v).expect("resource id to be present").to_string();
277 (v, resource_id)
278 })
279 .collect();
280 let not_in_existing = not_in_existing
281 .into_iter()
282 .map(|v| {
283 let resource_id = new_meta.get(&v).expect("resource id to be present").to_string();
284 (v, resource_id)
285 })
286 .collect();
287
288 Ok(StackDiff {
289 unchanged_ids: in_existing,
290 ids_to_be_removed: removed,
291 new_ids: not_in_existing,
292 })
293 }
294
295 fn update_resource_ids_for_existing_stack(&mut self, existing_ids_with_resource_ids: HashMap<String, String>) {
296 let still_existing_after_proposed_changes: Vec<_> = existing_ids_with_resource_ids
297 .into_iter()
298 .filter(|(existing_id, _)| self.metadata.contains_key(existing_id))
299 .map(|existing| {
300 let current_stack_resource_id = self
301 .metadata
302 .get(&existing.0)
303 .expect("existence to be checked by filter")
304 .to_string();
305 (existing.0, existing.1, current_stack_resource_id)
306 })
307 .collect();
308
309 still_existing_after_proposed_changes
310 .into_iter()
311 .for_each(|(existing_id, existing_resource_id, current_stack_resource_id)| {
312 let removed = self
313 .resources
314 .remove(¤t_stack_resource_id)
315 .expect("resource to exist in stack resources");
316 self.resources.insert(existing_resource_id.clone(), removed);
317 self.metadata.insert(existing_id, existing_resource_id.clone());
318 self.resource_ids_to_replace
319 .push((current_stack_resource_id.to_string(), existing_resource_id));
320 });
321 }
322
323 fn get_metadata(existing_stack: &str) -> Result<StackOnlyMetadata, String> {
324 serde_json::from_str(existing_stack).map_err(|e| format!("Could not retrieve resource info from existing stack: {}", e))
325 }
326}
327
328#[derive(Debug, Serialize, Deserialize)]
329#[serde(untagged)]
330pub enum Resource {
331 ApiGatewayV2Api(ApiGatewayV2Api),
332 ApiGatewayV2Integration(ApiGatewayV2Integration),
333 ApiGatewayV2Route(ApiGatewayV2Route),
334 ApiGatewayV2Stage(ApiGatewayV2Stage),
335 AppSyncApi(AppSyncApi),
336 Application(Application),
337 Bucket(Bucket),
338 BucketNotification(BucketNotification),
339 BucketPolicy(BucketPolicy),
340 CachePolicy(CachePolicy),
341 ChannelNamespace(ChannelNamespace),
342 ConfigurationProfile(ConfigurationProfile),
343 DeploymentStrategy(DeploymentStrategy),
344 Distribution(Distribution),
345 Environment(Environment),
346 EventSourceMapping(EventSourceMapping),
347 Function(Function),
348 LogGroup(LogGroup),
349 OriginAccessControl(OriginAccessControl),
350 Permission(Permission),
351 Queue(Queue),
352 QueuePolicy(QueuePolicy),
353 Role(Role),
354 Secret(Secret),
355 Schedule(Schedule),
356 Subscription(Subscription),
357 Table(Table),
358 Topic(Topic),
359 TopicPolicy(TopicPolicy),
360}
361
362impl Resource {
363 pub fn get_id(&self) -> Id {
364 let id = match self {
365 Resource::ApiGatewayV2Api(r) => r.get_id(),
366 Resource::ApiGatewayV2Integration(r) => r.get_id(),
367 Resource::ApiGatewayV2Route(r) => r.get_id(),
368 Resource::ApiGatewayV2Stage(r) => r.get_id(),
369 Resource::AppSyncApi(r) => r.get_id(),
370 Resource::Application(r) => r.get_id(),
371 Resource::Bucket(r) => r.get_id(),
372 Resource::BucketNotification(r) => r.get_id(),
373 Resource::BucketPolicy(r) => r.get_id(),
374 Resource::CachePolicy(r) => r.get_id(),
375 Resource::ChannelNamespace(r) => r.get_id(),
376 Resource::ConfigurationProfile(r) => r.get_id(),
377 Resource::DeploymentStrategy(r) => r.get_id(),
378 Resource::Distribution(r) => r.get_id(),
379 Resource::Environment(r) => r.get_id(),
380 Resource::EventSourceMapping(r) => r.get_id(),
381 Resource::Function(r) => r.get_id(),
382 Resource::LogGroup(r) => r.get_id(),
383 Resource::OriginAccessControl(r) => r.get_id(),
384 Resource::Permission(r) => r.get_id(),
385 Resource::Queue(r) => r.get_id(),
386 Resource::QueuePolicy(r) => r.get_id(),
387 Resource::Role(r) => r.get_id(),
388 Resource::Schedule(r) => r.get_id(),
389 Resource::Secret(r) => r.get_id(),
390 Resource::Subscription(r) => r.get_id(),
391 Resource::Table(r) => r.get_id(),
392 Resource::Topic(r) => r.get_id(),
393 Resource::TopicPolicy(r) => r.get_id(),
394 };
395 id.clone()
396 }
397
398 pub fn get_resource_id(&self) -> &str {
399 match self {
400 Resource::ApiGatewayV2Api(r) => r.get_resource_id(),
401 Resource::ApiGatewayV2Integration(r) => r.get_resource_id(),
402 Resource::ApiGatewayV2Route(r) => r.get_resource_id(),
403 Resource::ApiGatewayV2Stage(r) => r.get_resource_id(),
404 Resource::AppSyncApi(r) => r.get_resource_id(),
405 Resource::Application(r) => r.get_resource_id(),
406 Resource::Bucket(r) => r.get_resource_id(),
407 Resource::BucketNotification(r) => r.get_resource_id(),
408 Resource::BucketPolicy(r) => r.get_resource_id(),
409 Resource::CachePolicy(r) => r.get_resource_id(),
410 Resource::ChannelNamespace(r) => r.get_resource_id(),
411 Resource::ConfigurationProfile(r) => r.get_resource_id(),
412 Resource::DeploymentStrategy(r) => r.get_resource_id(),
413 Resource::Distribution(r) => r.get_resource_id(),
414 Resource::Environment(r) => r.get_resource_id(),
415 Resource::EventSourceMapping(r) => r.get_resource_id(),
416 Resource::Function(r) => r.get_resource_id(),
417 Resource::LogGroup(r) => r.get_resource_id(),
418 Resource::OriginAccessControl(r) => r.get_resource_id(),
419 Resource::Permission(r) => r.get_resource_id(),
420 Resource::Queue(r) => r.get_resource_id(),
421 Resource::QueuePolicy(r) => r.get_resource_id(),
422 Resource::Role(r) => r.get_resource_id(),
423 Resource::Schedule(r) => r.get_resource_id(),
424 Resource::Secret(r) => r.get_resource_id(),
425 Resource::Subscription(r) => r.get_resource_id(),
426 Resource::Table(t) => t.get_resource_id(),
427 Resource::Topic(r) => r.get_resource_id(),
428 Resource::TopicPolicy(r) => r.get_resource_id(),
429 }
430 }
431
432 pub(crate) fn generate_id(resource_name: &str) -> String {
433 let mut rng = rand::rng();
434 let random_suffix: u32 = rng.random();
435 format!("{resource_name}{random_suffix}")
436 }
437}
438
439macro_rules! from_resource {
440 ($name:ident) => {
441 impl From<$name> for Resource {
442 fn from(value: $name) -> Self {
443 Resource::$name(value)
444 }
445 }
446 };
447}
448
449from_resource!(ApiGatewayV2Api);
450from_resource!(ApiGatewayV2Integration);
451from_resource!(ApiGatewayV2Route);
452from_resource!(ApiGatewayV2Stage);
453from_resource!(AppSyncApi);
454from_resource!(Application);
455from_resource!(Bucket);
456from_resource!(BucketNotification);
457from_resource!(BucketPolicy);
458from_resource!(CachePolicy);
459from_resource!(ChannelNamespace);
460from_resource!(ConfigurationProfile);
461from_resource!(DeploymentStrategy);
462from_resource!(Distribution);
463from_resource!(Environment);
464from_resource!(EventSourceMapping);
465from_resource!(Function);
466from_resource!(LogGroup);
467from_resource!(OriginAccessControl);
468from_resource!(Permission);
469from_resource!(Queue);
470from_resource!(QueuePolicy);
471from_resource!(Role);
472from_resource!(Secret);
473from_resource!(Schedule);
474from_resource!(Subscription);
475from_resource!(Table);
476from_resource!(Topic);
477from_resource!(TopicPolicy);
478
479#[cfg(test)]
480mod tests {
481 use crate::sns::TopicBuilder;
482 use crate::sqs::QueueBuilder;
483 use crate::stack::StackBuilder;
484 use std::collections::HashMap;
485
486 #[test]
487 fn should_do_nothing_for_empty_stack_and_empty_existing_ids() {
488 let mut stack_builder = StackBuilder::new().build().unwrap();
489 let existing_ids = HashMap::new();
490
491 stack_builder.update_resource_ids_for_existing_stack(existing_ids);
492
493 assert_eq!(stack_builder.resources.len(), 0);
494 assert_eq!(stack_builder.metadata.len(), 0);
495 assert_eq!(stack_builder.resource_ids_to_replace.len(), 0);
496 }
497
498 #[test]
499 fn should_do_nothing_for_empty_stack() {
500 let mut stack_builder = StackBuilder::new().build().unwrap();
501 let mut existing_ids = HashMap::new();
502 existing_ids.insert("fun".to_string(), "abc123".to_string());
503
504 stack_builder.update_resource_ids_for_existing_stack(existing_ids);
505
506 assert_eq!(stack_builder.resources.len(), 0);
507 assert_eq!(stack_builder.metadata.len(), 0);
508 assert_eq!(stack_builder.resource_ids_to_replace.len(), 0);
509 }
510
511 #[test]
512 fn should_replace_topic_resource_id_with_the_existing_id() {
513 let mut stack_builder = StackBuilder::new();
514 TopicBuilder::new("topic").build(&mut stack_builder);
515 let mut existing_ids = HashMap::new();
516 existing_ids.insert("topic".to_string(), "abc123".to_string());
517 let mut stack = stack_builder.build().unwrap();
518
519 stack.update_resource_ids_for_existing_stack(existing_ids);
520
521 assert_eq!(stack.resources.len(), 1);
522 assert_eq!(stack.resource_ids_to_replace.len(), 1);
523 assert_eq!(stack.metadata.len(), 1);
524 assert_eq!(stack.metadata.get("topic").unwrap(), &"abc123".to_string());
525 }
526
527 #[test]
528 fn should_replace_topic_resource_id_with_the_existing_id_keeping_new_queue_id() {
529 let mut stack_builder = StackBuilder::new();
530 TopicBuilder::new("topic").build(&mut stack_builder);
531 QueueBuilder::new("queue").standard_queue().build(&mut stack_builder);
532 let mut existing_ids = HashMap::new();
533 existing_ids.insert("topic".to_string(), "abc123".to_string());
534 let mut stack = stack_builder.build().unwrap();
535
536 stack.update_resource_ids_for_existing_stack(existing_ids);
537
538 assert_eq!(stack.resources.len(), 2);
539 assert_eq!(stack.resource_ids_to_replace.len(), 1);
540 assert_eq!(stack.metadata.len(), 2);
541 assert_eq!(stack.metadata.get("topic").unwrap(), &"abc123".to_string());
542 }
543
544 #[test]
545 fn should_produce_diff() {
546 let mut stack_builder = StackBuilder::new();
547 TopicBuilder::new("topic").build(&mut stack_builder);
548 QueueBuilder::new("queue").standard_queue().build(&mut stack_builder);
549 let stack = stack_builder.build().unwrap();
550
551 let diff = stack
552 .get_diff(r#"{"Metadata": { "queue": "Queue123", "bucket": "Bucket234" } }"#)
553 .expect("diff to work");
554
555 assert_eq!(
556 diff.new_ids,
557 vec![("topic".to_string(), stack.metadata.get("topic").unwrap().to_string())]
558 );
559 assert_eq!(diff.ids_to_be_removed, vec![("bucket".to_string(), "Bucket234".to_string())]);
560 assert_eq!(diff.unchanged_ids, vec![("queue".to_string(), "Queue123".to_string())]);
561 }
562}