1use async_trait::async_trait;
2use chrono::Utc;
3use http::StatusCode;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use fakecloud_core::delivery::DeliveryBus;
8use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
9use fakecloud_dynamodb::state::SharedDynamoDbState;
10use fakecloud_eventbridge::state::SharedEventBridgeState;
11use fakecloud_iam::state::SharedIamState;
12use fakecloud_logs::state::SharedLogsState;
13use fakecloud_s3::state::SharedS3State;
14use fakecloud_sns::state::SharedSnsState;
15use fakecloud_sqs::state::SharedSqsState;
16use fakecloud_ssm::state::SharedSsmState;
17
18use crate::resource_provisioner::ResourceProvisioner;
19use crate::state::{SharedCloudFormationState, Stack, StackResource};
20use crate::template;
21use crate::xml_responses;
22
23fn provision_stack_resources(
32 provisioner: &ResourceProvisioner,
33 resource_defs: &[template::ResourceDefinition],
34 template_body: &str,
35 parameters: &HashMap<String, String>,
36) -> Result<Vec<StackResource>, AwsServiceError> {
37 let mut resources = Vec::new();
38 let mut physical_ids: HashMap<String, String> = HashMap::new();
39 let mut pending: Vec<&template::ResourceDefinition> = resource_defs.iter().collect();
40 let max_passes = pending.len() + 1;
41
42 for _ in 0..max_passes {
43 if pending.is_empty() {
44 break;
45 }
46 let mut still_pending = Vec::new();
47 let mut made_progress = false;
48
49 for resource_def in pending {
50 let resolved_def = template::resolve_resource_properties(
51 resource_def,
52 template_body,
53 parameters,
54 &physical_ids,
55 )
56 .map_err(|e| {
57 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationError", e)
58 })?;
59
60 match provisioner.create_resource(&resolved_def) {
61 Ok(stack_resource) => {
62 physical_ids.insert(
63 stack_resource.logical_id.clone(),
64 stack_resource.physical_id.clone(),
65 );
66 resources.push(stack_resource);
67 made_progress = true;
68 }
69 Err(_) => still_pending.push(resource_def),
70 }
71 }
72
73 pending = still_pending;
74 if !made_progress && !pending.is_empty() {
75 let resource_def = pending[0];
78 let resolved_def = template::resolve_resource_properties(
79 resource_def,
80 template_body,
81 parameters,
82 &physical_ids,
83 )
84 .unwrap_or_else(|_| resource_def.clone());
85 let err = provisioner.create_resource(&resolved_def).unwrap_err();
86 for r in &resources {
87 let _ = provisioner.delete_resource(r);
88 }
89 return Err(AwsServiceError::aws_error(
90 StatusCode::BAD_REQUEST,
91 "ValidationError",
92 format!(
93 "Failed to create resource {}: {err}",
94 resource_def.logical_id
95 ),
96 ));
97 }
98 }
99
100 Ok(resources)
101}
102
103pub struct CloudFormationDeps {
105 pub sqs: SharedSqsState,
106 pub sns: SharedSnsState,
107 pub ssm: SharedSsmState,
108 pub iam: SharedIamState,
109 pub s3: SharedS3State,
110 pub eventbridge: SharedEventBridgeState,
111 pub dynamodb: SharedDynamoDbState,
112 pub logs: SharedLogsState,
113 pub delivery: Arc<DeliveryBus>,
114}
115
116pub struct CloudFormationService {
117 state: SharedCloudFormationState,
118 deps: CloudFormationDeps,
119}
120
121impl CloudFormationService {
122 pub fn new(state: SharedCloudFormationState, deps: CloudFormationDeps) -> Self {
123 Self { state, deps }
124 }
125
126 fn provisioner(&self, stack_id: &str) -> ResourceProvisioner {
127 let cf_state = self.state.read();
128 ResourceProvisioner {
129 sqs_state: self.deps.sqs.clone(),
130 sns_state: self.deps.sns.clone(),
131 ssm_state: self.deps.ssm.clone(),
132 iam_state: self.deps.iam.clone(),
133 s3_state: self.deps.s3.clone(),
134 eventbridge_state: self.deps.eventbridge.clone(),
135 dynamodb_state: self.deps.dynamodb.clone(),
136 logs_state: self.deps.logs.clone(),
137 delivery: self.deps.delivery.clone(),
138 account_id: cf_state.account_id.clone(),
139 region: cf_state.region.clone(),
140 stack_id: stack_id.to_string(),
141 }
142 }
143
144 fn get_param(req: &AwsRequest, key: &str) -> Option<String> {
145 if let Some(v) = req.query_params.get(key) {
147 return Some(v.clone());
148 }
149 let body_params = fakecloud_core::protocol::parse_query_body(&req.body);
151 body_params.get(key).cloned()
152 }
153
154 fn get_all_params(req: &AwsRequest) -> HashMap<String, String> {
155 let mut params = req.query_params.clone();
156 let body_params = fakecloud_core::protocol::parse_query_body(&req.body);
157 for (k, v) in body_params {
158 params.entry(k).or_insert(v);
159 }
160 params
161 }
162
163 fn extract_tags(params: &HashMap<String, String>) -> HashMap<String, String> {
164 let mut tags = HashMap::new();
165 for i in 1.. {
166 let key_param = format!("Tags.member.{i}.Key");
167 let value_param = format!("Tags.member.{i}.Value");
168 match (params.get(&key_param), params.get(&value_param)) {
169 (Some(k), Some(v)) => {
170 tags.insert(k.clone(), v.clone());
171 }
172 _ => break,
173 }
174 }
175 tags
176 }
177
178 fn extract_parameters(params: &HashMap<String, String>) -> HashMap<String, String> {
179 let mut result = HashMap::new();
180 for i in 1.. {
181 let key_param = format!("Parameters.member.{i}.ParameterKey");
182 let value_param = format!("Parameters.member.{i}.ParameterValue");
183 match (params.get(&key_param), params.get(&value_param)) {
184 (Some(k), Some(v)) => {
185 result.insert(k.clone(), v.clone());
186 }
187 _ => break,
188 }
189 }
190 result
191 }
192
193 fn extract_notification_arns(params: &HashMap<String, String>) -> Vec<String> {
194 let mut arns = Vec::new();
195 for i in 1.. {
196 let key = format!("NotificationARNs.member.{i}");
197 match params.get(&key) {
198 Some(arn) => arns.push(arn.clone()),
199 None => break,
200 }
201 }
202 arns
203 }
204
205 fn send_stack_notification(
206 delivery: &DeliveryBus,
207 notification_arns: &[String],
208 stack_name: &str,
209 stack_id: &str,
210 status: &str,
211 ) {
212 if notification_arns.is_empty() {
213 return;
214 }
215 let message = format!(
216 "StackId='{}'\nTimestamp='{}'\nEventId='{}'\nLogicalResourceId='{}'\nResourceStatus='{}'\nResourceType='AWS::CloudFormation::Stack'\nStackName='{}'",
217 stack_id,
218 chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ"),
219 uuid::Uuid::new_v4(),
220 stack_name,
221 status,
222 stack_name,
223 );
224 for arn in notification_arns {
225 delivery.publish_to_sns(arn, &message, Some("AWS CloudFormation Notification"));
226 }
227 }
228
229 fn create_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
230 let params = Self::get_all_params(req);
231
232 let stack_name = params.get("StackName").ok_or_else(|| {
233 AwsServiceError::aws_error(
234 StatusCode::BAD_REQUEST,
235 "ValidationError",
236 "StackName is required",
237 )
238 })?;
239
240 let template_body = params.get("TemplateBody").ok_or_else(|| {
241 AwsServiceError::aws_error(
242 StatusCode::BAD_REQUEST,
243 "ValidationError",
244 "TemplateBody is required",
245 )
246 })?;
247
248 {
250 let state = self.state.read();
251 if let Some(existing) = state.stacks.get(stack_name.as_str()) {
252 if existing.status != "DELETE_COMPLETE" {
253 return Err(AwsServiceError::aws_error(
254 StatusCode::BAD_REQUEST,
255 "AlreadyExistsException",
256 format!("Stack [{stack_name}] already exists"),
257 ));
258 }
259 }
260 }
261
262 let tags = Self::extract_tags(¶ms);
263 let parameters = Self::extract_parameters(¶ms);
264 let notification_arns = Self::extract_notification_arns(¶ms);
265
266 let parsed = template::parse_template(template_body, ¶meters).map_err(|e| {
268 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationError", e)
269 })?;
270
271 let stack_id = {
272 let state = self.state.read();
273 format!(
274 "arn:aws:cloudformation:{}:{}:stack/{}/{}",
275 state.region,
276 state.account_id,
277 stack_name,
278 uuid::Uuid::new_v4()
279 )
280 };
281
282 let provisioner = self.provisioner(&stack_id);
283 let resources =
284 provision_stack_resources(&provisioner, &parsed.resources, template_body, ¶meters)?;
285
286 let stack = Stack {
287 name: stack_name.clone(),
288 stack_id: stack_id.clone(),
289 template: template_body.clone(),
290 status: "CREATE_COMPLETE".to_string(),
291 resources,
292 parameters,
293 tags,
294 created_at: Utc::now(),
295 updated_at: None,
296 description: parsed.description,
297 notification_arns: notification_arns.clone(),
298 };
299
300 {
301 let mut state = self.state.write();
302 state.stacks.insert(stack_name.clone(), stack);
303 }
304
305 Self::send_stack_notification(
306 &self.deps.delivery,
307 ¬ification_arns,
308 stack_name,
309 &stack_id,
310 "CREATE_COMPLETE",
311 );
312
313 Ok(AwsResponse::xml(
314 StatusCode::OK,
315 xml_responses::create_stack_response(&stack_id, &req.request_id),
316 ))
317 }
318
319 fn delete_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
320 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
321 AwsServiceError::aws_error(
322 StatusCode::BAD_REQUEST,
323 "ValidationError",
324 "StackName is required",
325 )
326 })?;
327
328 let mut state = self.state.write();
329
330 let stack = state.stacks.values_mut().find(|s| {
332 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
333 });
334
335 if let Some(stack) = stack {
336 let stack_id = stack.stack_id.clone();
337 let stack_name_for_notif = stack.name.clone();
338 let notification_arns = stack.notification_arns.clone();
339 let resources: Vec<_> = stack.resources.clone();
340
341 drop(state);
344 let provisioner = self.provisioner(&stack_id);
345
346 for resource in resources.iter().rev() {
348 let _ = provisioner.delete_resource(resource);
349 }
350
351 let mut state = self.state.write();
353 if let Some(stack) = state.stacks.values_mut().find(|s| s.stack_id == stack_id) {
354 stack.status = "DELETE_COMPLETE".to_string();
355 stack.resources.clear();
356 }
357 drop(state);
358
359 Self::send_stack_notification(
360 &self.deps.delivery,
361 ¬ification_arns,
362 &stack_name_for_notif,
363 &stack_id,
364 "DELETE_COMPLETE",
365 );
366 }
367
368 Ok(AwsResponse::xml(
369 StatusCode::OK,
370 xml_responses::delete_stack_response(&req.request_id),
371 ))
372 }
373
374 fn describe_stacks(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
375 let stack_name = Self::get_param(req, "StackName");
376
377 let state = self.state.read();
378 let stacks: Vec<Stack> = if let Some(ref name) = stack_name {
379 state
380 .stacks
381 .values()
382 .filter(|s| {
383 (s.name == *name || s.stack_id == *name) && s.status != "DELETE_COMPLETE"
384 })
385 .cloned()
386 .collect()
387 } else {
388 state
389 .stacks
390 .values()
391 .filter(|s| s.status != "DELETE_COMPLETE")
392 .cloned()
393 .collect()
394 };
395
396 if let Some(ref name) = stack_name {
397 if stacks.is_empty() {
398 return Err(AwsServiceError::aws_error(
399 StatusCode::BAD_REQUEST,
400 "ValidationError",
401 format!("Stack with id {name} does not exist"),
402 ));
403 }
404 }
405
406 Ok(AwsResponse::xml(
407 StatusCode::OK,
408 xml_responses::describe_stacks_response(&stacks, &req.request_id),
409 ))
410 }
411
412 fn list_stacks(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
413 let state = self.state.read();
414 let stacks: Vec<Stack> = state.stacks.values().cloned().collect();
415
416 Ok(AwsResponse::xml(
417 StatusCode::OK,
418 xml_responses::list_stacks_response(&stacks, &req.request_id),
419 ))
420 }
421
422 fn list_stack_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
423 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
424 AwsServiceError::aws_error(
425 StatusCode::BAD_REQUEST,
426 "ValidationError",
427 "StackName is required",
428 )
429 })?;
430
431 let state = self.state.read();
432 let stack = state
433 .stacks
434 .values()
435 .find(|s| {
436 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
437 })
438 .ok_or_else(|| {
439 AwsServiceError::aws_error(
440 StatusCode::BAD_REQUEST,
441 "ValidationError",
442 format!("Stack [{stack_name}] does not exist"),
443 )
444 })?;
445
446 Ok(AwsResponse::xml(
447 StatusCode::OK,
448 xml_responses::list_stack_resources_response(&stack.resources, &req.request_id),
449 ))
450 }
451
452 fn describe_stack_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
453 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
454 AwsServiceError::aws_error(
455 StatusCode::BAD_REQUEST,
456 "ValidationError",
457 "StackName is required",
458 )
459 })?;
460
461 let state = self.state.read();
462 let stack = state
463 .stacks
464 .values()
465 .find(|s| {
466 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
467 })
468 .ok_or_else(|| {
469 AwsServiceError::aws_error(
470 StatusCode::BAD_REQUEST,
471 "ValidationError",
472 format!("Stack [{stack_name}] does not exist"),
473 )
474 })?;
475
476 Ok(AwsResponse::xml(
477 StatusCode::OK,
478 xml_responses::describe_stack_resources_response(
479 &stack.resources,
480 &stack.name,
481 &req.request_id,
482 ),
483 ))
484 }
485
486 fn update_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
487 let input = UpdateStackInput::from_params(req)?;
488 let parsed =
489 template::parse_template(&input.template_body, &input.parameters).map_err(|e| {
490 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationError", e)
491 })?;
492
493 let found_stack_id = {
495 let state = self.state.read();
496 state
497 .stacks
498 .values()
499 .find(|s| {
500 (s.name == input.stack_name || s.stack_id == input.stack_name)
501 && s.status != "DELETE_COMPLETE"
502 })
503 .map(|s| s.stack_id.clone())
504 .unwrap_or_default()
505 };
506
507 let provisioner = self.provisioner(&found_stack_id);
508
509 let mut state = self.state.write();
510 let stack = state
511 .stacks
512 .values_mut()
513 .find(|s| {
514 (s.name == input.stack_name || s.stack_id == input.stack_name)
515 && s.status != "DELETE_COMPLETE"
516 })
517 .ok_or_else(|| {
518 AwsServiceError::aws_error(
519 StatusCode::BAD_REQUEST,
520 "ValidationError",
521 format!("Stack [{}] does not exist", input.stack_name),
522 )
523 })?;
524
525 let update_result = apply_resource_updates(
526 stack,
527 &parsed.resources,
528 &input.template_body,
529 &input.parameters,
530 &provisioner,
531 );
532
533 let stack_id = stack.stack_id.clone();
534 stack.template = input.template_body.clone();
535 stack.status = if update_result.is_err() {
536 "UPDATE_FAILED".to_string()
537 } else {
538 "UPDATE_COMPLETE".to_string()
539 };
540 stack.parameters = input.parameters;
541 if !input.tags.is_empty() {
542 stack.tags = input.tags;
543 }
544 stack.updated_at = Some(Utc::now());
545 stack.description = parsed.description;
546 if !input.notification_arns.is_empty() {
547 stack.notification_arns = input.notification_arns.clone();
548 }
549 let notification_arns = stack.notification_arns.clone();
550 let stack_name_for_notif = stack.name.clone();
551
552 if let Err(error_msg) = update_result {
553 drop(state);
554 Self::send_stack_notification(
555 &self.deps.delivery,
556 ¬ification_arns,
557 &stack_name_for_notif,
558 &stack_id,
559 "UPDATE_FAILED",
560 );
561 return Err(AwsServiceError::aws_error(
562 StatusCode::BAD_REQUEST,
563 "ValidationError",
564 error_msg,
565 ));
566 }
567
568 drop(state);
569 Self::send_stack_notification(
570 &self.deps.delivery,
571 ¬ification_arns,
572 &stack_name_for_notif,
573 &stack_id,
574 "UPDATE_COMPLETE",
575 );
576
577 Ok(AwsResponse::xml(
578 StatusCode::OK,
579 xml_responses::update_stack_response(&stack_id, &req.request_id),
580 ))
581 }
582
583 fn get_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
584 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
585 AwsServiceError::aws_error(
586 StatusCode::BAD_REQUEST,
587 "ValidationError",
588 "StackName is required",
589 )
590 })?;
591
592 let state = self.state.read();
593 let stack = state
594 .stacks
595 .values()
596 .find(|s| {
597 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
598 })
599 .ok_or_else(|| {
600 AwsServiceError::aws_error(
601 StatusCode::BAD_REQUEST,
602 "ValidationError",
603 format!("Stack [{stack_name}] does not exist"),
604 )
605 })?;
606
607 Ok(AwsResponse::xml(
608 StatusCode::OK,
609 xml_responses::get_template_response(&stack.template, &req.request_id),
610 ))
611 }
612}
613
614#[async_trait]
615impl AwsService for CloudFormationService {
616 fn service_name(&self) -> &str {
617 "cloudformation"
618 }
619
620 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
621 match req.action.as_str() {
622 "CreateStack" => self.create_stack(&req),
623 "DeleteStack" => self.delete_stack(&req),
624 "DescribeStacks" => self.describe_stacks(&req),
625 "ListStacks" => self.list_stacks(&req),
626 "ListStackResources" => self.list_stack_resources(&req),
627 "DescribeStackResources" => self.describe_stack_resources(&req),
628 "UpdateStack" => self.update_stack(&req),
629 "GetTemplate" => self.get_template(&req),
630 _ => Err(AwsServiceError::action_not_implemented(
631 "cloudformation",
632 &req.action,
633 )),
634 }
635 }
636
637 fn supported_actions(&self) -> &[&str] {
638 &[
639 "CreateStack",
640 "DeleteStack",
641 "DescribeStacks",
642 "ListStacks",
643 "ListStackResources",
644 "DescribeStackResources",
645 "UpdateStack",
646 "GetTemplate",
647 ]
648 }
649}
650
651struct UpdateStackInput {
653 stack_name: String,
654 template_body: String,
655 parameters: HashMap<String, String>,
656 tags: HashMap<String, String>,
657 notification_arns: Vec<String>,
658}
659
660impl UpdateStackInput {
661 fn from_params(req: &AwsRequest) -> Result<Self, AwsServiceError> {
662 let params = CloudFormationService::get_all_params(req);
663
664 let stack_name = params
665 .get("StackName")
666 .ok_or_else(|| {
667 AwsServiceError::aws_error(
668 StatusCode::BAD_REQUEST,
669 "ValidationError",
670 "StackName is required",
671 )
672 })?
673 .to_string();
674
675 let template_body = params
676 .get("TemplateBody")
677 .ok_or_else(|| {
678 AwsServiceError::aws_error(
679 StatusCode::BAD_REQUEST,
680 "ValidationError",
681 "TemplateBody is required",
682 )
683 })?
684 .to_string();
685
686 Ok(Self {
687 stack_name,
688 template_body,
689 parameters: CloudFormationService::extract_parameters(¶ms),
690 tags: CloudFormationService::extract_tags(¶ms),
691 notification_arns: CloudFormationService::extract_notification_arns(¶ms),
692 })
693 }
694}
695
696fn apply_resource_updates(
699 stack: &mut crate::state::Stack,
700 new_resource_defs: &[template::ResourceDefinition],
701 template_body: &str,
702 parameters: &HashMap<String, String>,
703 provisioner: &crate::resource_provisioner::ResourceProvisioner,
704) -> Result<(), String> {
705 let old_logical_ids: std::collections::HashSet<String> = stack
706 .resources
707 .iter()
708 .map(|r| r.logical_id.clone())
709 .collect();
710 let new_logical_ids: std::collections::HashSet<String> = new_resource_defs
711 .iter()
712 .map(|r| r.logical_id.clone())
713 .collect();
714
715 let to_remove: Vec<_> = stack
717 .resources
718 .iter()
719 .filter(|r| !new_logical_ids.contains(&r.logical_id))
720 .cloned()
721 .collect();
722 for resource in &to_remove {
723 let _ = provisioner.delete_resource(resource);
724 }
725 stack
726 .resources
727 .retain(|r| new_logical_ids.contains(&r.logical_id));
728
729 let mut physical_ids: HashMap<String, String> = stack
731 .resources
732 .iter()
733 .map(|r| (r.logical_id.clone(), r.physical_id.clone()))
734 .collect();
735
736 for resource_def in new_resource_defs {
738 if !old_logical_ids.contains(&resource_def.logical_id) {
739 let resolved_def = template::resolve_resource_properties(
740 resource_def,
741 template_body,
742 parameters,
743 &physical_ids,
744 )
745 .map_err(|e| {
746 format!(
747 "Failed to resolve resource {}: {e}",
748 resource_def.logical_id
749 )
750 })?;
751
752 match provisioner.create_resource(&resolved_def) {
753 Ok(stack_resource) => {
754 physical_ids.insert(
755 stack_resource.logical_id.clone(),
756 stack_resource.physical_id.clone(),
757 );
758 stack.resources.push(stack_resource);
759 }
760 Err(e) => {
761 tracing::warn!(
762 "Failed to create resource {} during update: {e}",
763 resource_def.logical_id
764 );
765 return Err(format!(
766 "Failed to create resource {}: {e}",
767 resource_def.logical_id
768 ));
769 }
770 }
771 }
772 }
773
774 Ok(())
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780 use crate::state::CloudFormationState;
781 use http::HeaderMap;
782 use parking_lot::RwLock;
783 use std::sync::Arc;
784
785 fn make_service() -> CloudFormationService {
786 let cf_state = Arc::new(RwLock::new(CloudFormationState::new(
787 "123456789012",
788 "us-east-1",
789 )));
790 let deps = CloudFormationDeps {
791 sqs: Arc::new(RwLock::new(fakecloud_sqs::state::SqsState::new(
792 "123456789012",
793 "us-east-1",
794 "http://localhost:4566",
795 ))),
796 sns: Arc::new(RwLock::new(fakecloud_sns::state::SnsState::new(
797 "123456789012",
798 "us-east-1",
799 "http://localhost:4566",
800 ))),
801 ssm: Arc::new(RwLock::new(fakecloud_ssm::state::SsmState::new(
802 "123456789012",
803 "us-east-1",
804 ))),
805 iam: Arc::new(RwLock::new(fakecloud_iam::state::IamState::new(
806 "123456789012",
807 ))),
808 s3: Arc::new(RwLock::new(fakecloud_s3::state::S3State::new(
809 "123456789012",
810 "us-east-1",
811 ))),
812 eventbridge: Arc::new(RwLock::new(
813 fakecloud_eventbridge::state::EventBridgeState::new("123456789012", "us-east-1"),
814 )),
815 dynamodb: Arc::new(RwLock::new(fakecloud_dynamodb::state::DynamoDbState::new(
816 "123456789012",
817 "us-east-1",
818 ))),
819 logs: Arc::new(RwLock::new(fakecloud_logs::state::LogsState::new(
820 "123456789012",
821 "us-east-1",
822 ))),
823 delivery: Arc::new(DeliveryBus::new()),
824 };
825 CloudFormationService::new(cf_state, deps)
826 }
827
828 fn make_request(action: &str, params: HashMap<String, String>) -> AwsRequest {
829 AwsRequest {
830 service: "cloudformation".to_string(),
831 action: action.to_string(),
832 region: "us-east-1".to_string(),
833 account_id: "123456789012".to_string(),
834 request_id: "test-request-id".to_string(),
835 headers: HeaderMap::new(),
836 query_params: params,
837 body: bytes::Bytes::new(),
838 path_segments: vec![],
839 raw_path: "/".to_string(),
840 raw_query: String::new(),
841 method: http::Method::POST,
842 is_query_protocol: true,
843 access_key_id: None,
844 }
845 }
846
847 #[test]
848 fn update_stack_sets_failed_status_on_resource_error() {
849 let svc = make_service();
850
851 let mut create_params = HashMap::new();
853 create_params.insert("StackName".to_string(), "test-stack".to_string());
854 create_params.insert(
855 "TemplateBody".to_string(),
856 r#"{"Resources":{"MyQueue":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"q1"}}}}"#.to_string(),
857 );
858 let req = make_request("CreateStack", create_params);
859 let result = svc.create_stack(&req);
860 assert!(result.is_ok());
861
862 let mut update_params = HashMap::new();
864 update_params.insert("StackName".to_string(), "test-stack".to_string());
865 update_params.insert(
866 "TemplateBody".to_string(),
867 r#"{"Resources":{"MyQueue":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"q1"}},"BadSub":{"Type":"AWS::SNS::Subscription","Properties":{"TopicArn":"arn:aws:sns:us-east-1:123456789012:nope","Protocol":"sqs","Endpoint":"arn:aws:sqs:us-east-1:123456789012:q1"}}}}"#.to_string(),
868 );
869 let req = make_request("UpdateStack", update_params);
870 let result = svc.update_stack(&req);
871
872 assert!(result.is_err());
874
875 let state = svc.state.read();
877 let stack = state.stacks.get("test-stack").unwrap();
878 assert_eq!(stack.status, "UPDATE_FAILED");
879 }
880
881 #[test]
882 fn create_stack_resolves_ref_to_physical_id() {
883 let svc = make_service();
884
885 let template = r#"{
887 "Resources": {
888 "MyTopic": {
889 "Type": "AWS::SNS::Topic",
890 "Properties": { "TopicName": "ref-test-topic" }
891 },
892 "MySub": {
893 "Type": "AWS::SNS::Subscription",
894 "Properties": {
895 "TopicArn": { "Ref": "MyTopic" },
896 "Protocol": "sqs",
897 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:some-queue"
898 }
899 }
900 }
901 }"#;
902
903 let mut params = HashMap::new();
904 params.insert("StackName".to_string(), "ref-stack".to_string());
905 params.insert("TemplateBody".to_string(), template.to_string());
906 let req = make_request("CreateStack", params);
907 let result = svc.create_stack(&req);
908 assert!(result.is_ok(), "CreateStack failed: {:?}", result.err());
909
910 let state = svc.state.read();
912 let stack = state.stacks.get("ref-stack").unwrap();
913 assert_eq!(stack.resources.len(), 2);
914 assert_eq!(stack.status, "CREATE_COMPLETE");
915
916 let sub = stack
918 .resources
919 .iter()
920 .find(|r| r.logical_id == "MySub")
921 .unwrap();
922 assert!(
923 sub.physical_id.contains("ref-test-topic"),
924 "Subscription physical ID should reference the topic ARN, got: {}",
925 sub.physical_id
926 );
927 }
928}