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