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