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::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 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 still_existing_after_proposed_changes: Vec<_> = existing_ids_with_resource_ids
264 .into_iter()
265 .filter(|(existing_id, _)| self.metadata.contains_key(existing_id))
266 .map(|existing| {
267 let current_stack_resource_id = self.metadata.get(&existing.0).expect("existence to be checked by filter").to_string();
268 (existing.0, existing.1, current_stack_resource_id)
269 })
270 .collect();
271
272 still_existing_after_proposed_changes.into_iter().for_each(|(existing_id, existing_resource_id, current_stack_resource_id)| {
273 let removed = self
274 .resources
275 .remove(¤t_stack_resource_id)
276 .expect("resource to exist in stack resources");
277 self.resources.insert(existing_resource_id.clone(), removed);
278 self.metadata.insert(existing_id, existing_resource_id.clone());
279 self.resource_ids_to_replace
280 .push((current_stack_resource_id.to_string(), existing_resource_id));
281 });
282 }
283
284 fn get_metadata(existing_stack: &str) -> Result<StackOnlyMetadata, String> {
285 serde_json::from_str(existing_stack).map_err(|e| format!("Could not retrieve resource info from existing stack: {}", e))
286 }
287}
288
289#[derive(Debug, Serialize, Deserialize)]
290#[serde(untagged)]
291pub enum Resource {
292 ApiGatewayV2Api(ApiGatewayV2Api),
293 ApiGatewayV2Integration(ApiGatewayV2Integration),
294 ApiGatewayV2Route(ApiGatewayV2Route),
295 ApiGatewayV2Stage(ApiGatewayV2Stage),
296 AppSyncApi(AppSyncApi),
297 Application(Application),
298 Bucket(Bucket),
299 BucketNotification(BucketNotification),
300 BucketPolicy(BucketPolicy),
301 CachePolicy(CachePolicy),
302 ChannelNamespace(ChannelNamespace),
303 ConfigurationProfile(ConfigurationProfile),
304 DeploymentStrategy(DeploymentStrategy),
305 Distribution(Distribution),
306 Environment(Environment),
307 EventSourceMapping(EventSourceMapping),
308 Function(Function),
309 LogGroup(LogGroup),
310 OriginAccessControl(OriginAccessControl),
311 Permission(Permission),
312 Queue(Queue),
313 QueuePolicy(QueuePolicy),
314 Role(Role),
315 Secret(Secret),
316 Schedule(Schedule),
317 Subscription(Subscription),
318 Table(Table),
319 Topic(Topic),
320 TopicPolicy(TopicPolicy),
321}
322
323impl Resource {
324 pub fn get_id(&self) -> Id {
325 let id = match self {
326 Resource::ApiGatewayV2Api(r) => r.get_id(),
327 Resource::ApiGatewayV2Integration(r) => r.get_id(),
328 Resource::ApiGatewayV2Route(r) => r.get_id(),
329 Resource::ApiGatewayV2Stage(r) => r.get_id(),
330 Resource::AppSyncApi(r) => r.get_id(),
331 Resource::Application(r) => r.get_id(),
332 Resource::Bucket(r) => r.get_id(),
333 Resource::BucketNotification(r) => r.get_id(),
334 Resource::BucketPolicy(r) => r.get_id(),
335 Resource::CachePolicy(r) => r.get_id(),
336 Resource::ChannelNamespace(r) => r.get_id(),
337 Resource::ConfigurationProfile(r) => r.get_id(),
338 Resource::DeploymentStrategy(r) => r.get_id(),
339 Resource::Distribution(r) => r.get_id(),
340 Resource::Environment(r) => r.get_id(),
341 Resource::EventSourceMapping(r) => r.get_id(),
342 Resource::Function(r) => r.get_id(),
343 Resource::LogGroup(r) => r.get_id(),
344 Resource::OriginAccessControl(r) => r.get_id(),
345 Resource::Permission(r) => r.get_id(),
346 Resource::Queue(r) => r.get_id(),
347 Resource::QueuePolicy(r) => r.get_id(),
348 Resource::Role(r) => r.get_id(),
349 Resource::Schedule(r) => r.get_id(),
350 Resource::Secret(r) => r.get_id(),
351 Resource::Subscription(r) => r.get_id(),
352 Resource::Table(r) => r.get_id(),
353 Resource::Topic(r) => r.get_id(),
354 Resource::TopicPolicy(r) => r.get_id(),
355 };
356 id.clone()
357 }
358
359 pub fn get_resource_id(&self) -> &str {
360 match self {
361 Resource::ApiGatewayV2Api(r) => r.get_resource_id(),
362 Resource::ApiGatewayV2Integration(r) => r.get_resource_id(),
363 Resource::ApiGatewayV2Route(r) => r.get_resource_id(),
364 Resource::ApiGatewayV2Stage(r) => r.get_resource_id(),
365 Resource::AppSyncApi(r) => r.get_resource_id(),
366 Resource::Application(r) => r.get_resource_id(),
367 Resource::Bucket(r) => r.get_resource_id(),
368 Resource::BucketNotification(r) => r.get_resource_id(),
369 Resource::BucketPolicy(r) => r.get_resource_id(),
370 Resource::CachePolicy(r) => r.get_resource_id(),
371 Resource::ChannelNamespace(r) => r.get_resource_id(),
372 Resource::ConfigurationProfile(r) => r.get_resource_id(),
373 Resource::DeploymentStrategy(r) => r.get_resource_id(),
374 Resource::Distribution(r) => r.get_resource_id(),
375 Resource::Environment(r) => r.get_resource_id(),
376 Resource::EventSourceMapping(r) => r.get_resource_id(),
377 Resource::Function(r) => r.get_resource_id(),
378 Resource::LogGroup(r) => r.get_resource_id(),
379 Resource::OriginAccessControl(r) => r.get_resource_id(),
380 Resource::Permission(r) => r.get_resource_id(),
381 Resource::Queue(r) => r.get_resource_id(),
382 Resource::QueuePolicy(r) => r.get_resource_id(),
383 Resource::Role(r) => r.get_resource_id(),
384 Resource::Schedule(r) => r.get_resource_id(),
385 Resource::Secret(r) => r.get_resource_id(),
386 Resource::Subscription(r) => r.get_resource_id(),
387 Resource::Table(t) => t.get_resource_id(),
388 Resource::Topic(r) => r.get_resource_id(),
389 Resource::TopicPolicy(r) => r.get_resource_id(),
390 }
391 }
392
393 pub(crate) fn generate_id(resource_name: &str) -> String {
394 let mut rng = rand::rng();
395 let random_suffix: u32 = rng.random();
396 format!("{resource_name}{random_suffix}")
397 }
398}
399
400macro_rules! from_resource {
401 ($name:ident) => {
402 impl From<$name> for Resource {
403 fn from(value: $name) -> Self {
404 Resource::$name(value)
405 }
406 }
407 };
408}
409
410from_resource!(ApiGatewayV2Api);
411from_resource!(ApiGatewayV2Integration);
412from_resource!(ApiGatewayV2Route);
413from_resource!(ApiGatewayV2Stage);
414from_resource!(AppSyncApi);
415from_resource!(Application);
416from_resource!(Bucket);
417from_resource!(BucketNotification);
418from_resource!(BucketPolicy);
419from_resource!(CachePolicy);
420from_resource!(ChannelNamespace);
421from_resource!(ConfigurationProfile);
422from_resource!(DeploymentStrategy);
423from_resource!(Distribution);
424from_resource!(Environment);
425from_resource!(EventSourceMapping);
426from_resource!(Function);
427from_resource!(LogGroup);
428from_resource!(OriginAccessControl);
429from_resource!(Permission);
430from_resource!(Queue);
431from_resource!(QueuePolicy);
432from_resource!(Role);
433from_resource!(Secret);
434from_resource!(Schedule);
435from_resource!(Subscription);
436from_resource!(Table);
437from_resource!(Topic);
438from_resource!(TopicPolicy);
439
440#[cfg(test)]
441mod tests {
442 use crate::sns::TopicBuilder;
443 use crate::sqs::QueueBuilder;
444 use crate::stack::StackBuilder;
445 use std::collections::HashMap;
446
447 #[test]
448 fn should_do_nothing_for_empty_stack_and_empty_existing_ids() {
449 let mut stack_builder = StackBuilder::new().build().unwrap();
450 let existing_ids = HashMap::new();
451
452 stack_builder.update_resource_ids_for_existing_stack(existing_ids);
453
454 assert_eq!(stack_builder.resources.len(), 0);
455 assert_eq!(stack_builder.metadata.len(), 0);
456 assert_eq!(stack_builder.resource_ids_to_replace.len(), 0);
457 }
458
459 #[test]
460 fn should_do_nothing_for_empty_stack() {
461 let mut stack_builder = StackBuilder::new().build().unwrap();
462 let mut existing_ids = HashMap::new();
463 existing_ids.insert("fun".to_string(), "abc123".to_string());
464
465 stack_builder.update_resource_ids_for_existing_stack(existing_ids);
466
467 assert_eq!(stack_builder.resources.len(), 0);
468 assert_eq!(stack_builder.metadata.len(), 0);
469 assert_eq!(stack_builder.resource_ids_to_replace.len(), 0);
470 }
471
472 #[test]
473 fn should_replace_topic_resource_id_with_the_existing_id() {
474 let mut stack_builder = StackBuilder::new();
475 TopicBuilder::new("topic").build(&mut stack_builder);
476 let mut existing_ids = HashMap::new();
477 existing_ids.insert("topic".to_string(), "abc123".to_string());
478 let mut stack = stack_builder.build().unwrap();
479
480 stack.update_resource_ids_for_existing_stack(existing_ids);
481
482 assert_eq!(stack.resources.len(), 1);
483 assert_eq!(stack.resource_ids_to_replace.len(), 1);
484 assert_eq!(stack.metadata.len(), 1);
485 assert_eq!(stack.metadata.get("topic").unwrap(), &"abc123".to_string());
486 }
487
488 #[test]
489 fn should_replace_topic_resource_id_with_the_existing_id_keeping_new_queue_id() {
490 let mut stack_builder = StackBuilder::new();
491 TopicBuilder::new("topic").build(&mut stack_builder);
492 QueueBuilder::new("queue").standard_queue().build(&mut stack_builder);
493 let mut existing_ids = HashMap::new();
494 existing_ids.insert("topic".to_string(), "abc123".to_string());
495 let mut stack = stack_builder.build().unwrap();
496
497 stack.update_resource_ids_for_existing_stack(existing_ids);
498
499 assert_eq!(stack.resources.len(), 2);
500 assert_eq!(stack.resource_ids_to_replace.len(), 1);
501 assert_eq!(stack.metadata.len(), 2);
502 assert_eq!(stack.metadata.get("topic").unwrap(), &"abc123".to_string());
503 }
504
505 #[test]
506 fn should_produce_diff() {
507 let mut stack_builder = StackBuilder::new();
508 TopicBuilder::new("topic").build(&mut stack_builder);
509 QueueBuilder::new("queue").standard_queue().build(&mut stack_builder);
510 let stack = stack_builder.build().unwrap();
511
512 let diff = stack
513 .get_diff(r#"{"Metadata": { "queue": "Queue123", "bucket": "Bucket234" } }"#)
514 .expect("diff to work");
515
516 assert_eq!(
517 diff.new_ids,
518 vec![("topic".to_string(), stack.metadata.get("topic").unwrap().to_string())]
519 );
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}