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