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