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