1use crate::apigateway::{ApiGatewayV2Api, ApiGatewayV2Integration, ApiGatewayV2Route, ApiGatewayV2Stage};
2use crate::cloudwatch::LogGroup;
3use crate::dynamodb::Table;
4use crate::iam::Role;
5use crate::lambda::{EventSourceMapping, Function, Permission};
6use crate::s3::{Bucket, BucketPolicy};
7use crate::secretsmanager::Secret;
8use crate::shared::Id;
9use crate::sns::{Subscription, Topic, TopicPolicy};
10use crate::sqs::{Queue, QueuePolicy};
11use rand::Rng;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::fmt::{Display, Formatter};
15use crate::appconfig::{Application, ConfigurationProfile, DeploymentStrategy, Environment};
16use crate::appsync::{AppSyncApi, ChannelNamespace};
17use crate::cloudfront::{CachePolicy, Distribution, OriginAccessControl};
18use crate::custom_resource::BucketNotification;
19
20#[derive(Debug, Clone)]
21pub struct Asset {
22 pub s3_bucket: String,
23 pub s3_key: String,
24 pub path: String,
25}
26
27impl Display for Asset {
28 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
29 f.write_fmt(format_args!("Asset at path {} for bucket {} and key {}", self.path, self.s3_bucket, self.s3_key))
30 }
31}
32
33#[derive(Debug, Serialize)]
73pub struct Stack {
74 #[serde(skip)]
75 pub(crate) to_replace: Vec<(String, String)>,
76 #[serde(skip)]
77 pub(crate) tags: Vec<(String, String)>,
78 #[serde(rename = "Resources")]
79 pub(crate) resources: HashMap<String, Resource>,
80 #[serde(rename = "Metadata")]
81 pub(crate) metadata: HashMap<String, String>,
82}
83
84#[derive(Debug, Deserialize)]
85struct StackOnlyMetadata {
86 #[serde(rename = "Metadata")]
87 pub(crate) metadata: HashMap<String, String>,
88}
89
90impl Stack {
91 pub fn get_tags(&self) -> Vec<(String, String)> {
92 self.tags.clone()
93 }
94
95 pub fn get_assets(&self) -> Vec<Asset> {
96 self.resources
97 .values()
98 .flat_map(|r| match r {
99 Resource::Function(l) => l.asset.clone(), _ => None,
101 })
102 .collect()
103 }
104
105 pub fn synth(&self) -> Result<String, String> {
147 let mut naive_synth = serde_json::to_string(self).map_err(|e| format!("Could not serialize stack: {e:#?}"))?;
148 self.to_replace.iter().for_each(|(current, new)| {
150 naive_synth = naive_synth.replace(current, new);
151 });
152
153 Ok(naive_synth)
154 }
155
156 pub fn synth_for_existing(&mut self, existing_stack: &str) -> Result<String, String> {
207 let meta: StackOnlyMetadata = serde_json::from_str(existing_stack).map_err(|_| {
208 "Could not retrieve resource info from existing stack".to_string()
209 })?;
210 self.update_resource_ids_for_existing_stack(meta.metadata);
211 self.synth()
212 }
213
214 fn update_resource_ids_for_existing_stack(&mut self, existing_ids_with_resource_ids: HashMap<String, String>) {
215 let current_ids: HashMap<String, String> = self
216 .resources
217 .iter()
218 .map(|(resource_id, resource)| (resource.get_id().0, resource_id.to_string()))
219 .collect();
220
221 existing_ids_with_resource_ids
222 .into_iter()
223 .filter(|(existing_id, _)| current_ids.contains_key(existing_id))
224 .for_each(|(existing_id, existing_resource_id)| {
225 let current_stack_resource_id = current_ids.get(&existing_id).expect("existence to be checked by filter");
226 let removed = self
227 .resources
228 .remove(current_stack_resource_id)
229 .expect("resource to exist in stack resources");
230 self.resources.insert(existing_resource_id.clone(), removed);
231 self.metadata.insert(existing_id, existing_resource_id.clone());
232 self.to_replace.push((current_stack_resource_id.to_string(), existing_resource_id));
233 });
234 }
235}
236
237#[derive(Debug, Serialize)]
238#[serde(untagged)]
239pub enum Resource {
240 AppSyncApi(AppSyncApi),
241 Application(Application),
242 Bucket(Bucket),
243 BucketPolicy(BucketPolicy),
244 BucketNotification(BucketNotification),
245 ChannelNamespace(ChannelNamespace),
246 ConfigurationProfile(ConfigurationProfile),
247 DeploymentStrategy(DeploymentStrategy),
248 Environment(Environment),
249 Table(Table),
250 Function(Function),
251 LogGroup(LogGroup),
252 Queue(Queue),
253 QueuePolicy(QueuePolicy),
254 Topic(Topic),
255 TopicPolicy(TopicPolicy),
256 Subscription(Subscription),
257 Permission(Permission),
258 EventSourceMapping(EventSourceMapping),
259 Role(Role),
260 ApiGatewayV2Api(ApiGatewayV2Api),
261 ApiGatewayV2Stage(ApiGatewayV2Stage),
262 ApiGatewayV2Route(ApiGatewayV2Route),
263 ApiGatewayV2Integration(ApiGatewayV2Integration),
264 Secret(Secret),
265 Distribution(Distribution),
266 CachePolicy(CachePolicy),
267 OriginAccessControl(OriginAccessControl),
268}
269
270impl Resource {
271 pub fn get_id(&self) -> Id {
272 let id = match self {
273 Resource::Bucket(r) => r.get_id(),
274 Resource::BucketPolicy(r) => r.get_id(),
275 Resource::Table(r) => r.get_id(),
276 Resource::Function(r) => r.get_id(),
277 Resource::LogGroup(r) => r.get_id(),
278 Resource::Queue(r) => r.get_id(),
279 Resource::Topic(r) => r.get_id(),
280 Resource::Subscription(r) => r.get_id(),
281 Resource::Permission(r) => r.get_id(),
282 Resource::EventSourceMapping(r) => r.get_id(),
283 Resource::Role(r) => r.get_id(),
284 Resource::ApiGatewayV2Api(r) => r.get_id(),
285 Resource::ApiGatewayV2Stage(r) => r.get_id(),
286 Resource::ApiGatewayV2Route(r) => r.get_id(),
287 Resource::ApiGatewayV2Integration(r) => r.get_id(),
288 Resource::Secret(r) => r.get_id(),
289 Resource::Distribution(r) => r.get_id(),
290 Resource::CachePolicy(r) => r.get_id(),
291 Resource::OriginAccessControl(r) => r.get_id(),
292 Resource::Application(r) => r.get_id(),
293 Resource::ConfigurationProfile(r) => r.get_id(),
294 Resource::DeploymentStrategy(r) => r.get_id(),
295 Resource::Environment(r) => r.get_id(),
296 Resource::BucketNotification(r) => r.get_id(),
297 Resource::TopicPolicy(r) => r.get_id(),
298 Resource::QueuePolicy(r) => r.get_id(),
299 Resource::AppSyncApi(r) => r.get_id(),
300 Resource::ChannelNamespace(r) => r.get_id(),
301 };
302 id.clone()
303 }
304
305 pub fn get_resource_id(&self) -> &str {
306 match self {
307 Resource::Bucket(r) => r.get_resource_id(),
308 Resource::BucketPolicy(r) => r.get_resource_id(),
309 Resource::Table(t) => t.get_resource_id(),
310 Resource::Function(r) => r.get_resource_id(),
311 Resource::Role(r) => r.get_resource_id(),
312 Resource::Queue(r) => r.get_resource_id(),
313 Resource::EventSourceMapping(r) => r.get_resource_id(),
314 Resource::LogGroup(r) => r.get_resource_id(),
315 Resource::Topic(r) => r.get_resource_id(),
316 Resource::Subscription(r) => r.get_resource_id(),
317 Resource::Permission(r) => r.get_resource_id(),
318 Resource::ApiGatewayV2Api(r) => r.get_resource_id(),
319 Resource::ApiGatewayV2Stage(r) => r.get_resource_id(),
320 Resource::ApiGatewayV2Route(r) => r.get_resource_id(),
321 Resource::ApiGatewayV2Integration(r) => r.get_resource_id(),
322 Resource::Secret(r) => r.get_resource_id(),
323 Resource::Distribution(r) => r.get_resource_id(),
324 Resource::CachePolicy(r) => r.get_resource_id(),
325 Resource::OriginAccessControl(r) => r.get_resource_id(),
326 Resource::Application(r) => r.get_resource_id(),
327 Resource::ConfigurationProfile(r) => r.get_resource_id(),
328 Resource::DeploymentStrategy(r) => r.get_resource_id(),
329 Resource::Environment(r) => r.get_resource_id(),
330 Resource::BucketNotification(r) => r.get_resource_id(),
331 Resource::TopicPolicy(r) => r.get_resource_id(),
332 Resource::QueuePolicy(r) => r.get_resource_id(),
333 Resource::AppSyncApi(r) => r.get_resource_id(),
334 Resource::ChannelNamespace(r) => r.get_resource_id(),
335 }
336 }
337
338 pub(crate) fn generate_id(resource_name: &str) -> String {
339 let mut rng = rand::rng();
340 let random_suffix: u32 = rng.random();
341 format!("{resource_name}{random_suffix}")
342 }
343}
344
345macro_rules! from_resource {
346 ($name:ident) => {
347 impl From<$name> for Resource {
348 fn from(value: $name) -> Self {
349 Resource::$name(value)
350 }
351 }
352 };
353}
354
355from_resource!(AppSyncApi);
356from_resource!(Application);
357from_resource!(Bucket);
358from_resource!(BucketPolicy);
359from_resource!(ConfigurationProfile);
360from_resource!(ChannelNamespace);
361from_resource!(DeploymentStrategy);
362from_resource!(Environment);
363from_resource!(Table);
364from_resource!(Function);
365from_resource!(Role);
366from_resource!(LogGroup);
367from_resource!(Queue);
368from_resource!(QueuePolicy);
369from_resource!(Topic);
370from_resource!(TopicPolicy);
371from_resource!(EventSourceMapping);
372from_resource!(Permission);
373from_resource!(Subscription);
374from_resource!(ApiGatewayV2Api);
375from_resource!(ApiGatewayV2Stage);
376from_resource!(ApiGatewayV2Route);
377from_resource!(ApiGatewayV2Integration);
378from_resource!(Secret);
379from_resource!(Distribution);
380from_resource!(CachePolicy);
381from_resource!(OriginAccessControl);
382from_resource!(BucketNotification);
383
384#[cfg(test)]
385mod tests {
386 use crate::sns::TopicBuilder;
387 use crate::sqs::QueueBuilder;
388 use crate::stack::StackBuilder;
389 use std::collections::HashMap;
390
391 #[test]
392 fn should_do_nothing_for_empty_stack_and_empty_existing_ids() {
393 let mut stack_builder = StackBuilder::new().build().unwrap();
394 let existing_ids = HashMap::new();
395
396 stack_builder.update_resource_ids_for_existing_stack(existing_ids);
397
398 assert_eq!(stack_builder.resources.len(), 0);
399 assert_eq!(stack_builder.metadata.len(), 0);
400 assert_eq!(stack_builder.to_replace.len(), 0);
401 }
402
403 #[test]
404 fn should_do_nothing_for_empty_stack() {
405 let mut stack_builder = StackBuilder::new().build().unwrap();
406 let mut existing_ids = HashMap::new();
407 existing_ids.insert("fun".to_string(), "abc123".to_string());
408
409 stack_builder.update_resource_ids_for_existing_stack(existing_ids);
410
411 assert_eq!(stack_builder.resources.len(), 0);
412 assert_eq!(stack_builder.metadata.len(), 0);
413 assert_eq!(stack_builder.to_replace.len(), 0);
414 }
415
416 #[test]
417 fn should_replace_topic_resource_id_with_the_existing_id() {
418 let mut stack_builder = StackBuilder::new();
419 TopicBuilder::new("topic").build(&mut stack_builder);
420 let mut existing_ids = HashMap::new();
421 existing_ids.insert("topic".to_string(), "abc123".to_string());
422 let mut stack = stack_builder.build().unwrap();
423
424 stack.update_resource_ids_for_existing_stack(existing_ids);
425
426 assert_eq!(stack.resources.len(), 1);
427 assert_eq!(stack.to_replace.len(), 1);
428 assert_eq!(stack.metadata.len(), 1);
429 assert_eq!(stack.metadata.get("topic").unwrap(), &"abc123".to_string());
430 }
431
432 #[test]
433 fn should_replace_topic_resource_id_with_the_existing_id_keeping_new_queue_id() {
434 let mut stack_builder = StackBuilder::new();
435 TopicBuilder::new("topic").build(&mut stack_builder);
436 QueueBuilder::new("queue").standard_queue().build(&mut stack_builder);
437 let mut existing_ids = HashMap::new();
438 existing_ids.insert("topic".to_string(), "abc123".to_string());
439 let mut stack = stack_builder.build().unwrap();
440
441 stack.update_resource_ids_for_existing_stack(existing_ids);
442
443 assert_eq!(stack.resources.len(), 2);
444 assert_eq!(stack.to_replace.len(), 1);
445 assert_eq!(stack.metadata.len(), 2);
446 assert_eq!(stack.metadata.get("topic").unwrap(), &"abc123".to_string());
447 }
448}