1use chrono::Utc;
14use http::StatusCode;
15use serde_json::{json, Value};
16use std::collections::BTreeMap;
17
18use fakecloud_aws::arn::Arn;
19use fakecloud_aws::xml::xml_escape;
20use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
21
22use crate::service::CloudFormationService;
23use crate::state::{Stack, StackResource};
24use crate::template;
25
26const NS: &str = "http://cloudformation.amazonaws.com/doc/2010-05-15/";
27
28fn rand_id() -> String {
29 use std::sync::atomic::{AtomicU64, Ordering};
30 static COUNTER: AtomicU64 = AtomicU64::new(0);
31 let nanos = std::time::SystemTime::now()
32 .duration_since(std::time::UNIX_EPOCH)
33 .map(|d| d.as_nanos())
34 .unwrap_or(0);
35 let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
36 format!("{nanos:x}-{seq:x}")
37}
38
39fn xml_response(action: &str, inner: String, request_id: &str) -> AwsResponse {
40 let body = format!(
41 r#"<{action}Response xmlns="{NS}">
42 <{action}Result>
43{inner}
44 </{action}Result>
45 <ResponseMetadata>
46 <RequestId>{rid}</RequestId>
47 </ResponseMetadata>
48</{action}Response>"#,
49 action = action,
50 NS = NS,
51 inner = inner,
52 rid = xml_escape(request_id),
53 );
54 AwsResponse::xml(StatusCode::OK, body)
55}
56
57fn xml_response_no_result(action: &str, request_id: &str) -> AwsResponse {
58 let body = format!(
59 r#"<{action}Response xmlns="{NS}">
60 <ResponseMetadata>
61 <RequestId>{rid}</RequestId>
62 </ResponseMetadata>
63</{action}Response>"#,
64 action = action,
65 NS = NS,
66 rid = xml_escape(request_id),
67 );
68 AwsResponse::xml(StatusCode::OK, body)
69}
70
71fn members_xml<F>(items: &[Value], render: F) -> String
72where
73 F: Fn(&Value) -> String,
74{
75 items
76 .iter()
77 .map(|v| format!(" <member>\n{}\n </member>", render(v)))
78 .collect::<Vec<_>>()
79 .join("\n")
80}
81
82fn store<'a>(
83 extras: &'a mut BTreeMap<String, BTreeMap<String, Value>>,
84 category: &str,
85) -> &'a mut BTreeMap<String, Value> {
86 extras.entry(category.to_string()).or_default()
87}
88
89fn missing(name: &str) -> AwsServiceError {
90 AwsServiceError::aws_error(
91 StatusCode::BAD_REQUEST,
92 "ValidationError",
93 format!("{name} is required"),
94 )
95}
96
97fn has_collection_param(params: &BTreeMap<String, String>, field: &str) -> bool {
103 let prefix = format!("{field}.");
104 params.keys().any(|k| k.starts_with(&prefix))
105}
106
107fn require_scalar(params: &BTreeMap<String, String>, field: &str) -> Result<(), AwsServiceError> {
112 if params.get(field).is_some() {
113 Ok(())
114 } else {
115 Err(missing(field))
116 }
117}
118
119fn require_collection(
120 params: &BTreeMap<String, String>,
121 field: &str,
122) -> Result<(), AwsServiceError> {
123 if has_collection_param(params, field) {
124 Ok(())
125 } else {
126 Err(missing(field))
127 }
128}
129
130fn parse_s3_url(url: &str) -> Option<(String, String)> {
136 let rest = url.split_once("://").map(|(_, r)| r).unwrap_or(url);
137 let (host, after) = rest.split_once('/')?;
138 let path = after.split(['?', '#']).next().unwrap_or(after);
139 if let Some(idx) = host.find(".s3") {
142 let bucket = &host[..idx];
143 if !bucket.is_empty() {
144 return Some((bucket.to_string(), path.to_string()));
145 }
146 }
147 let (bucket, key) = path.split_once('/')?;
149 if bucket.is_empty() || key.is_empty() {
150 return None;
151 }
152 Some((bucket.to_string(), key.to_string()))
153}
154
155impl CloudFormationService {
156 fn fetch_template_from_url(&self, account_id: &str, url: &str) -> Option<String> {
162 let (bucket, key) = parse_s3_url(url)?;
163 let mut accounts = self.deps.s3.write();
164 let state = accounts.get_or_create(account_id);
165 let body_ref = {
166 let b = state.buckets.get(&bucket)?;
167 b.objects.get(&key)?.body.clone()
168 };
169 let bytes = state.read_body(&body_ref).ok()?;
170 String::from_utf8(bytes.to_vec()).ok()
171 }
172
173 pub(crate) fn handle_extra_action(
174 &self,
175 req: &AwsRequest,
176 ) -> Result<AwsResponse, AwsServiceError> {
177 let action = req.action.clone();
178 let params = Self::get_all_params(req);
179 let aid = req.account_id.clone();
180 let rid = req.request_id.clone();
181
182 match action.as_str() {
183 "CreateChangeSet" => {
185 let stack_name = params
186 .get("StackName")
187 .ok_or_else(|| missing("StackName"))?
188 .clone();
189 let cs_name = params
190 .get("ChangeSetName")
191 .ok_or_else(|| missing("ChangeSetName"))?
192 .clone();
193 let template_body = {
200 let inline = params.get("TemplateBody").cloned().unwrap_or_default();
201 if inline.trim().is_empty() {
202 params
203 .get("TemplateURL")
204 .and_then(|url| self.fetch_template_from_url(&aid, url))
205 .unwrap_or(inline)
206 } else {
207 inline
208 }
209 };
210 let change_set_type = params
214 .get("ChangeSetType")
215 .map(|s| s.to_ascii_uppercase())
216 .unwrap_or_else(|| "UPDATE".to_string());
217
218 let cs_params = CloudFormationService::extract_parameters(¶ms);
219 let cs_tags = CloudFormationService::extract_tags(¶ms);
220 let cs_notif = CloudFormationService::extract_notification_arns(¶ms);
221
222 let stack_lookup: Option<(String, Vec<crate::state::StackResource>)> = {
226 let accounts = self.state.read();
227 accounts.get(&aid).and_then(|s| {
228 s.stacks
229 .values()
230 .find(|st| {
231 (st.name == stack_name || st.stack_id == stack_name)
232 && st.status != "DELETE_COMPLETE"
233 })
234 .map(|st| (st.stack_id.clone(), st.resources.clone()))
235 })
236 };
237
238 let mut full_params: BTreeMap<String, String> = cs_params.clone();
241 full_params
242 .entry("AWS::Region".to_string())
243 .or_insert_with(|| req.region.clone());
244 full_params
245 .entry("AWS::AccountId".to_string())
246 .or_insert_with(|| aid.clone());
247 full_params
248 .entry("AWS::StackName".to_string())
249 .or_insert_with(|| stack_name.clone());
250 full_params
251 .entry("AWS::Partition".to_string())
252 .or_insert_with(|| "aws".to_string());
253 full_params
254 .entry("AWS::URLSuffix".to_string())
255 .or_insert_with(|| "amazonaws.com".to_string());
256 if let Some((sid, _)) = &stack_lookup {
257 full_params
258 .entry("AWS::StackId".to_string())
259 .or_insert_with(|| sid.clone());
260 }
261
262 let mut changes: Vec<Value> = Vec::new();
267 if !template_body.trim().is_empty() {
268 let parsed =
275 template::parse_template(&template_body, &full_params).unwrap_or_default();
276
277 let existing_resources = stack_lookup
278 .as_ref()
279 .map(|(_, r)| r.clone())
280 .unwrap_or_default();
281 let existing_by_id: BTreeMap<&str, &crate::state::StackResource> =
282 existing_resources
283 .iter()
284 .map(|r| (r.logical_id.as_str(), r))
285 .collect();
286 let new_by_id: BTreeMap<&str, &template::ResourceDefinition> = parsed
287 .resources
288 .iter()
289 .map(|r| (r.logical_id.as_str(), r))
290 .collect();
291
292 for r in &parsed.resources {
293 if let Some(existing) = existing_by_id.get(r.logical_id.as_str()) {
294 let replacement = if existing.resource_type != r.resource_type {
295 "True"
296 } else {
297 "Conditional"
298 };
299 changes.push(json!({
300 "Type": "Resource",
301 "ResourceChange": {
302 "Action": "Modify",
303 "LogicalResourceId": r.logical_id,
304 "PhysicalResourceId": existing.physical_id,
305 "ResourceType": r.resource_type,
306 "Replacement": replacement,
307 }
308 }));
309 } else {
310 changes.push(json!({
311 "Type": "Resource",
312 "ResourceChange": {
313 "Action": "Add",
314 "LogicalResourceId": r.logical_id,
315 "ResourceType": r.resource_type,
316 }
317 }));
318 }
319 }
320 for r in &existing_resources {
321 if !new_by_id.contains_key(r.logical_id.as_str()) {
322 changes.push(json!({
323 "Type": "Resource",
324 "ResourceChange": {
325 "Action": "Remove",
326 "LogicalResourceId": r.logical_id,
327 "PhysicalResourceId": r.physical_id,
328 "ResourceType": r.resource_type,
329 }
330 }));
331 }
332 }
333 }
334
335 let id = Arn::new(
336 "cloudformation",
337 "us-east-1",
338 &aid,
339 &format!("changeSet/{cs_name}/{}", rand_id()),
340 )
341 .to_string();
342 let stack_id_str = stack_lookup
343 .as_ref()
344 .map(|(s, _)| s.clone())
345 .unwrap_or_else(|| {
346 Arn::new(
347 "cloudformation",
348 "us-east-1",
349 &aid,
350 &format!("stack/{stack_name}/{}", rand_id()),
351 )
352 .to_string()
353 });
354
355 let entry = json!({
356 "Id": id,
357 "ChangeSetName": cs_name,
358 "StackId": stack_id_str,
359 "StackName": stack_name,
360 "Status": "CREATE_COMPLETE",
361 "ExecutionStatus": "AVAILABLE",
362 "TemplateBody": template_body,
363 "Parameters": cs_params,
364 "Tags": cs_tags,
365 "NotificationArns": cs_notif,
366 "Changes": changes,
367 });
368 let mut accounts = self.state.write();
369 let state = accounts.get_or_create(&aid);
370 if change_set_type == "CREATE" {
376 let live_status = state
382 .stacks
383 .values()
384 .find(|s| {
385 (s.name == stack_name || s.stack_id == stack_name)
386 && s.status != "DELETE_COMPLETE"
387 })
388 .map(|s| s.status.clone());
389 match live_status.as_deref() {
390 None => {
393 state.stacks.insert(
394 stack_name.clone(),
395 Stack {
396 name: stack_name.clone(),
397 stack_id: stack_id_str.clone(),
398 template: String::new(),
399 status: "REVIEW_IN_PROGRESS".to_string(),
400 resources: Vec::new(),
401 parameters: BTreeMap::new(),
402 tags: BTreeMap::new(),
403 created_at: Utc::now(),
404 updated_at: None,
405 description: None,
406 notification_arns: Vec::new(),
407 outputs: Vec::new(),
408 },
409 );
410 }
411 Some("REVIEW_IN_PROGRESS") => {}
415 Some(_) => {
418 return Err(AwsServiceError::aws_error(
419 StatusCode::BAD_REQUEST,
420 "AlreadyExistsException",
421 format!("Stack [{stack_name}] already exists"),
422 ));
423 }
424 }
425 }
426 store(&mut state.extras, "change_sets").insert(id.clone(), entry);
427 Ok(xml_response(
428 "CreateChangeSet",
429 format!(
430 " <Id>{}</Id>\n <StackId>{}</StackId>",
431 xml_escape(&id),
432 xml_escape(&stack_id_str)
433 ),
434 &rid,
435 ))
436 }
437 "DescribeChangeSet" => {
438 let cs = params
439 .get("ChangeSetName")
440 .ok_or_else(|| missing("ChangeSetName"))?
441 .clone();
442 let stack_filter = params.get("StackName").cloned();
443 let accounts = self.state.read();
444 let entry = accounts.get(&aid)
445 .and_then(|s| s.extras.get("change_sets"))
446 .and_then(|m| m.values().find(|v| {
447 let id_match = v["Id"].as_str() == Some(&cs)
448 || v["ChangeSetName"].as_str() == Some(&cs);
449 let stack_match = stack_filter.as_deref().is_none_or(|sf| {
450 v["StackName"].as_str() == Some(sf)
451 || v["StackId"].as_str() == Some(sf)
452 });
453 id_match && stack_match
454 }))
455 .cloned()
456 .unwrap_or_else(|| json!({"ChangeSetName": cs.clone(), "Status": "CREATE_COMPLETE", "ExecutionStatus": "AVAILABLE"}));
457 let changes_xml = entry["Changes"]
458 .as_array()
459 .map(|arr| {
460 let mut out = String::new();
461 for change in arr {
462 let rc = &change["ResourceChange"];
463 out.push_str(" <member>\n");
464 out.push_str(&format!(
465 " <Type>{}</Type>\n",
466 xml_escape(change["Type"].as_str().unwrap_or("Resource"))
467 ));
468 out.push_str(" <ResourceChange>\n");
469 out.push_str(&format!(
470 " <Action>{}</Action>\n",
471 xml_escape(rc["Action"].as_str().unwrap_or(""))
472 ));
473 out.push_str(&format!(
474 " <LogicalResourceId>{}</LogicalResourceId>\n",
475 xml_escape(rc["LogicalResourceId"].as_str().unwrap_or(""))
476 ));
477 if let Some(pid) = rc["PhysicalResourceId"].as_str() {
478 out.push_str(&format!(
479 " <PhysicalResourceId>{}</PhysicalResourceId>\n",
480 xml_escape(pid)
481 ));
482 }
483 out.push_str(&format!(
484 " <ResourceType>{}</ResourceType>\n",
485 xml_escape(rc["ResourceType"].as_str().unwrap_or(""))
486 ));
487 if let Some(replacement) = rc["Replacement"].as_str() {
488 out.push_str(&format!(
489 " <Replacement>{}</Replacement>\n",
490 xml_escape(replacement)
491 ));
492 }
493 out.push_str(" </ResourceChange>\n");
494 out.push_str(" </member>");
495 out.push('\n');
496 }
497 out
498 })
499 .unwrap_or_default();
500 let inner = format!(
501 " <ChangeSetName>{}</ChangeSetName>\n <ChangeSetId>{}</ChangeSetId>\n <StackId>{}</StackId>\n <StackName>{}</StackName>\n <Status>{}</Status>\n <ExecutionStatus>{}</ExecutionStatus>\n <Changes>\n{} </Changes>",
502 xml_escape(entry["ChangeSetName"].as_str().unwrap_or("")),
503 xml_escape(entry["Id"].as_str().unwrap_or("")),
504 xml_escape(entry["StackId"].as_str().unwrap_or("")),
505 xml_escape(entry["StackName"].as_str().unwrap_or("")),
506 xml_escape(entry["Status"].as_str().unwrap_or("CREATE_COMPLETE")),
507 xml_escape(entry["ExecutionStatus"].as_str().unwrap_or("AVAILABLE")),
508 changes_xml,
509 );
510 Ok(xml_response("DescribeChangeSet", inner, &rid))
511 }
512 "DescribeChangeSetHooks" => {
513 require_scalar(¶ms, "ChangeSetName")?;
514 Ok(xml_response(
515 "DescribeChangeSetHooks",
516 " <Hooks/>".to_string(),
517 &rid,
518 ))
519 }
520 "DeleteChangeSet" => {
521 let cs = params
522 .get("ChangeSetName")
523 .ok_or_else(|| missing("ChangeSetName"))?
524 .clone();
525 let mut accounts = self.state.write();
526 let state = accounts.get_or_create(&aid);
527 if let Some(m) = state.extras.get_mut("change_sets") {
528 m.retain(|_, v| {
529 v["Id"].as_str() != Some(&cs) && v["ChangeSetName"].as_str() != Some(&cs)
530 });
531 }
532 Ok(xml_response("DeleteChangeSet", String::new(), &rid))
533 }
534 "ExecuteChangeSet" => {
535 let cs = params
536 .get("ChangeSetName")
537 .cloned()
538 .ok_or_else(|| missing("ChangeSetName"))?;
539 let stack_filter = params.get("StackName").cloned();
540
541 let entry = {
542 let accounts = self.state.read();
543 accounts
544 .get(&aid)
545 .and_then(|s| s.extras.get("change_sets"))
546 .and_then(|m| {
547 m.values()
548 .find(|v| {
549 let id_match = v["Id"].as_str() == Some(&cs)
550 || v["ChangeSetName"].as_str() == Some(&cs);
551 let stack_match = stack_filter.as_deref().is_none_or(|sf| {
552 v["StackName"].as_str() == Some(sf)
553 || v["StackId"].as_str() == Some(sf)
554 });
555 id_match && stack_match
556 })
557 .cloned()
558 })
559 };
560 let Some(entry) = entry else {
561 return Ok(xml_response("ExecuteChangeSet", String::new(), &rid));
565 };
566
567 if entry["ExecutionStatus"].as_str() != Some("AVAILABLE") {
568 return Err(AwsServiceError::aws_error(
569 StatusCode::BAD_REQUEST,
570 "InvalidChangeSetStatus",
571 format!(
572 "ChangeSet [{cs}] cannot be executed in its current status of [{}]",
573 entry["ExecutionStatus"].as_str().unwrap_or("")
574 ),
575 ));
576 }
577
578 let cs_id = entry["Id"].as_str().unwrap_or("").to_string();
579 let stack_name = entry["StackName"].as_str().unwrap_or("").to_string();
580 let template_body = entry["TemplateBody"].as_str().unwrap_or("").to_string();
581
582 let cs_tags: BTreeMap<String, String> = entry["Tags"]
583 .as_object()
584 .map(|m| {
585 m.iter()
586 .filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string())))
587 .collect()
588 })
589 .unwrap_or_default();
590 let cs_notif: Vec<String> = entry["NotificationArns"]
591 .as_array()
592 .map(|a| {
593 a.iter()
594 .filter_map(|v| v.as_str().map(|s| s.to_string()))
595 .collect()
596 })
597 .unwrap_or_default();
598 let mut cs_params: BTreeMap<String, String> = entry["Parameters"]
599 .as_object()
600 .map(|m| {
601 m.iter()
602 .filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string())))
603 .collect()
604 })
605 .unwrap_or_default();
606
607 let found: Option<String> = {
608 let accounts = self.state.read();
609 accounts.get(&aid).and_then(|s| {
610 s.stacks
611 .values()
612 .find(|st| {
613 (st.name == stack_name || st.stack_id == stack_name)
614 && st.status != "DELETE_COMPLETE"
615 })
616 .map(|st| st.stack_id.clone())
617 })
618 };
619
620 if template_body.trim().is_empty() {
627 let mut accounts = self.state.write();
628 let state = accounts.get_or_create(&aid);
629 if let Some(sid) = &found {
630 if let Some(stack) = state.stacks.values_mut().find(|s| &s.stack_id == sid)
631 {
632 if stack.status == "REVIEW_IN_PROGRESS" {
633 stack.status = "CREATE_COMPLETE".to_string();
634 stack.updated_at = Some(Utc::now());
635 }
636 }
637 }
638 if let Some(m) = state.extras.get_mut("change_sets") {
639 if let Some(e) = m.get_mut(&cs_id) {
640 e["ExecutionStatus"] = json!("EXECUTE_COMPLETE");
641 }
642 }
643 return Ok(xml_response("ExecuteChangeSet", String::new(), &rid));
644 }
645
646 let found_stack_id = found.ok_or_else(|| {
650 AwsServiceError::aws_error(
651 StatusCode::BAD_REQUEST,
652 "ValidationError",
653 format!("Stack [{stack_name}] does not exist"),
654 )
655 })?;
656
657 cs_params
658 .entry("AWS::Region".to_string())
659 .or_insert_with(|| req.region.clone());
660 cs_params
661 .entry("AWS::AccountId".to_string())
662 .or_insert_with(|| aid.clone());
663 cs_params
664 .entry("AWS::StackId".to_string())
665 .or_insert_with(|| found_stack_id.clone());
666 cs_params
667 .entry("AWS::StackName".to_string())
668 .or_insert_with(|| stack_name.clone());
669 cs_params
670 .entry("AWS::Partition".to_string())
671 .or_insert_with(|| "aws".to_string());
672 cs_params
673 .entry("AWS::URLSuffix".to_string())
674 .or_insert_with(|| "amazonaws.com".to_string());
675
676 let parsed = if template_body.trim().is_empty() {
680 template::ParsedTemplate {
681 description: None,
682 resources: Vec::new(),
683 outputs: Vec::new(),
684 }
685 } else {
686 template::parse_template(&template_body, &cs_params).map_err(|e| {
687 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationError", e)
688 })?
689 };
690
691 let provisioner = self.provisioner(&found_stack_id, &aid, &req.region);
692
693 let mut accounts = self.state.write();
694 let state = accounts.get_or_create(&aid);
695
696 let (update_result, sid, stack_name_owned, was_review) = {
703 let stack = state
704 .stacks
705 .values_mut()
706 .find(|st| st.stack_id == found_stack_id && st.status != "DELETE_COMPLETE")
707 .ok_or_else(|| {
708 AwsServiceError::aws_error(
709 StatusCode::BAD_REQUEST,
710 "ValidationError",
711 format!("Stack [{stack_name}] does not exist"),
712 )
713 })?;
714 let was_review = stack.status == "REVIEW_IN_PROGRESS";
715 stack.status = if was_review {
716 "CREATE_IN_PROGRESS"
717 } else {
718 "UPDATE_IN_PROGRESS"
719 }
720 .to_string();
721 let result = crate::service::apply_resource_updates(
722 stack,
723 &parsed.resources,
724 &template_body,
725 &cs_params,
726 &provisioner,
727 );
728 let sid = stack.stack_id.clone();
729 let sname = stack.name.clone();
730 stack.template = template_body.clone();
731 stack.status = match (was_review, result.is_err()) {
732 (true, false) => "CREATE_COMPLETE",
733 (true, true) => "ROLLBACK_COMPLETE",
734 (false, false) => "UPDATE_COMPLETE",
735 (false, true) => "UPDATE_ROLLBACK_COMPLETE",
736 }
737 .to_string();
738 stack.parameters = cs_params.clone();
739 if !cs_tags.is_empty() {
740 stack.tags = cs_tags;
741 }
742 if !cs_notif.is_empty() {
743 stack.notification_arns = cs_notif;
744 }
745 stack.updated_at = Some(Utc::now());
746 if result.is_ok() {
747 stack.outputs.clear();
748 }
749 (result, sid, sname, was_review)
750 };
751
752 let (in_progress, complete, failed) = if was_review {
754 ("CREATE_IN_PROGRESS", "CREATE_COMPLETE", "ROLLBACK_COMPLETE")
755 } else {
756 (
757 "UPDATE_IN_PROGRESS",
758 "UPDATE_COMPLETE",
759 "UPDATE_ROLLBACK_COMPLETE",
760 )
761 };
762 crate::service::record_stack_status_event(
763 state,
764 &sid,
765 &stack_name_owned,
766 "AWS::CloudFormation::Stack",
767 in_progress,
768 );
769 let final_status = match &update_result {
770 Ok(changes) => {
771 crate::service::record_stack_events(
772 state,
773 &sid,
774 &stack_name_owned,
775 changes,
776 );
777 complete
778 }
779 Err(_) => failed,
780 };
781 crate::service::record_stack_status_event(
782 state,
783 &sid,
784 &stack_name_owned,
785 "AWS::CloudFormation::Stack",
786 final_status,
787 );
788
789 if let Some(m) = state.extras.get_mut("change_sets") {
790 if let Some(e) = m.get_mut(&cs_id) {
791 e["ExecutionStatus"] = json!(if update_result.is_err() {
792 "EXECUTE_FAILED"
793 } else {
794 "EXECUTE_COMPLETE"
795 });
796 }
797 }
798
799 drop(accounts);
800
801 if let Err(msg) = update_result {
802 return Err(AwsServiceError::aws_error(
803 StatusCode::BAD_REQUEST,
804 "ValidationError",
805 msg,
806 ));
807 }
808
809 Ok(xml_response("ExecuteChangeSet", String::new(), &rid))
810 }
811 "ListChangeSets" => {
812 require_scalar(¶ms, "StackName")?;
813 let accounts = self.state.read();
814 let items: Vec<Value> = accounts
815 .get(&aid)
816 .and_then(|s| s.extras.get("change_sets"))
817 .map(|m| m.values().cloned().collect())
818 .unwrap_or_default();
819 let inner = format!(
820 " <Summaries>\n{}\n </Summaries>",
821 members_xml(&items, |v| {
822 format!(
823 " <ChangeSetId>{}</ChangeSetId>\n <ChangeSetName>{}</ChangeSetName>\n <Status>{}</Status>",
824 xml_escape(v["Id"].as_str().unwrap_or("")),
825 xml_escape(v["ChangeSetName"].as_str().unwrap_or("")),
826 xml_escape(v["Status"].as_str().unwrap_or("CREATE_COMPLETE")),
827 )
828 }),
829 );
830 Ok(xml_response("ListChangeSets", inner, &rid))
831 }
832
833 "CreateStackSet" => {
835 let name = params
836 .get("StackSetName")
837 .ok_or_else(|| missing("StackSetName"))?
838 .clone();
839 let id = format!("{name}:{}", rand_id());
840 let entry = json!({
841 "StackSetId": id,
842 "StackSetName": name,
843 "Status": "ACTIVE",
844 "TemplateBody": params.get("TemplateBody").cloned().unwrap_or_default(),
845 });
846 let mut accounts = self.state.write();
847 let state = accounts.get_or_create(&aid);
848 store(&mut state.extras, "stack_sets").insert(name.clone(), entry);
849 Ok(xml_response(
850 "CreateStackSet",
851 format!(" <StackSetId>{}</StackSetId>", xml_escape(&id)),
852 &rid,
853 ))
854 }
855 "DescribeStackSet" => {
856 let name = params
857 .get("StackSetName")
858 .ok_or_else(|| missing("StackSetName"))?
859 .clone();
860 let accounts = self.state.read();
861 let entry = accounts
862 .get(&aid)
863 .and_then(|s| s.extras.get("stack_sets"))
864 .and_then(|m| m.get(&name))
865 .cloned()
866 .unwrap_or_else(|| json!({"StackSetName": name.clone(), "Status": "ACTIVE"}));
867 let inner = format!(
868 " <StackSet>\n <StackSetName>{}</StackSetName>\n <StackSetId>{}</StackSetId>\n <Status>{}</Status>\n </StackSet>",
869 xml_escape(entry["StackSetName"].as_str().unwrap_or(&name)),
870 xml_escape(entry["StackSetId"].as_str().unwrap_or("")),
871 xml_escape(entry["Status"].as_str().unwrap_or("ACTIVE")),
872 );
873 Ok(xml_response("DescribeStackSet", inner, &rid))
874 }
875 "ListStackSets" => {
876 let accounts = self.state.read();
877 let items: Vec<Value> = accounts
878 .get(&aid)
879 .and_then(|s| s.extras.get("stack_sets"))
880 .map(|m| m.values().cloned().collect())
881 .unwrap_or_default();
882 let inner = format!(
883 " <Summaries>\n{}\n </Summaries>",
884 members_xml(&items, |v| {
885 format!(
886 " <StackSetName>{}</StackSetName>\n <StackSetId>{}</StackSetId>\n <Status>{}</Status>",
887 xml_escape(v["StackSetName"].as_str().unwrap_or("")),
888 xml_escape(v["StackSetId"].as_str().unwrap_or("")),
889 xml_escape(v["Status"].as_str().unwrap_or("ACTIVE")),
890 )
891 }),
892 );
893 Ok(xml_response("ListStackSets", inner, &rid))
894 }
895 "UpdateStackSet" => {
896 require_scalar(¶ms, "StackSetName")?;
897 let op_id = rand_id();
898 Ok(xml_response(
899 "UpdateStackSet",
900 format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
901 &rid,
902 ))
903 }
904 "DeleteStackSet" => {
905 let name = params
906 .get("StackSetName")
907 .ok_or_else(|| missing("StackSetName"))?
908 .clone();
909 let mut accounts = self.state.write();
910 let state = accounts.get_or_create(&aid);
911 if let Some(m) = state.extras.get_mut("stack_sets") {
912 m.remove(&name);
913 }
914 Ok(xml_response("DeleteStackSet", String::new(), &rid))
915 }
916 "DescribeStackSetOperation" => {
917 require_scalar(¶ms, "StackSetName")?;
918 require_scalar(¶ms, "OperationId")?;
919 let op_id = params.get("OperationId").cloned().unwrap_or_else(rand_id);
920 let inner = format!(
921 " <StackSetOperation>\n <OperationId>{}</OperationId>\n <Status>SUCCEEDED</Status>\n </StackSetOperation>",
922 xml_escape(&op_id),
923 );
924 Ok(xml_response("DescribeStackSetOperation", inner, &rid))
925 }
926 "ListStackSetOperations" => {
927 require_scalar(¶ms, "StackSetName")?;
928 Ok(xml_response(
929 "ListStackSetOperations",
930 " <Summaries/>".to_string(),
931 &rid,
932 ))
933 }
934 "ListStackSetOperationResults" => {
935 require_scalar(¶ms, "StackSetName")?;
936 require_scalar(¶ms, "OperationId")?;
937 Ok(xml_response(
938 "ListStackSetOperationResults",
939 " <Summaries/>".to_string(),
940 &rid,
941 ))
942 }
943 "ListStackSetAutoDeploymentTargets" => {
944 require_scalar(¶ms, "StackSetName")?;
945 Ok(xml_response(
946 "ListStackSetAutoDeploymentTargets",
947 " <Summaries/>".to_string(),
948 &rid,
949 ))
950 }
951 "StopStackSetOperation" => {
952 require_scalar(¶ms, "StackSetName")?;
953 require_scalar(¶ms, "OperationId")?;
954 Ok(xml_response("StopStackSetOperation", String::new(), &rid))
955 }
956 "ImportStacksToStackSet" => {
957 require_scalar(¶ms, "StackSetName")?;
958 let op_id = rand_id();
959 Ok(xml_response(
960 "ImportStacksToStackSet",
961 format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
962 &rid,
963 ))
964 }
965
966 "CreateStackInstances" => {
974 require_scalar(¶ms, "StackSetName")?;
975 let op_id = rand_id();
976 Ok(xml_response(
977 "CreateStackInstances",
978 format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
979 &rid,
980 ))
981 }
982 "UpdateStackInstances" => {
983 require_scalar(¶ms, "StackSetName")?;
984 let op_id = rand_id();
985 Ok(xml_response(
986 "UpdateStackInstances",
987 format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
988 &rid,
989 ))
990 }
991 "DeleteStackInstances" => {
992 require_scalar(¶ms, "StackSetName")?;
993 require_scalar(¶ms, "RetainStacks")?;
994 let op_id = rand_id();
995 Ok(xml_response(
996 "DeleteStackInstances",
997 format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
998 &rid,
999 ))
1000 }
1001 "DescribeStackInstance" => {
1002 require_scalar(¶ms, "StackSetName")?;
1003 require_scalar(¶ms, "StackInstanceAccount")?;
1004 require_scalar(¶ms, "StackInstanceRegion")?;
1005 let inner =
1006 " <StackInstance>\n <Status>CURRENT</Status>\n </StackInstance>"
1007 .to_string();
1008 Ok(xml_response("DescribeStackInstance", inner, &rid))
1009 }
1010 "ListStackInstances" => {
1011 require_scalar(¶ms, "StackSetName")?;
1012 Ok(xml_response(
1013 "ListStackInstances",
1014 " <Summaries/>".to_string(),
1015 &rid,
1016 ))
1017 }
1018 "ListStackInstanceResourceDrifts" => {
1019 require_scalar(¶ms, "StackSetName")?;
1020 require_scalar(¶ms, "StackInstanceAccount")?;
1021 require_scalar(¶ms, "StackInstanceRegion")?;
1022 require_scalar(¶ms, "OperationId")?;
1023 Ok(xml_response(
1024 "ListStackInstanceResourceDrifts",
1025 " <Summaries/>".to_string(),
1026 &rid,
1027 ))
1028 }
1029
1030 "CreateStackRefactor" => {
1032 require_collection(¶ms, "StackDefinitions")?;
1033 let id = rand_id();
1034 let entry = json!({"StackRefactorId": id.clone(), "Status": "CREATE_COMPLETE"});
1035 let mut accounts = self.state.write();
1036 let state = accounts.get_or_create(&aid);
1037 store(&mut state.extras, "refactors").insert(id.clone(), entry);
1038 Ok(xml_response(
1039 "CreateStackRefactor",
1040 format!(" <StackRefactorId>{}</StackRefactorId>", xml_escape(&id)),
1041 &rid,
1042 ))
1043 }
1044 "DescribeStackRefactor" => {
1045 let id = params
1046 .get("StackRefactorId")
1047 .ok_or_else(|| missing("StackRefactorId"))?
1048 .clone();
1049 let inner = format!(
1050 " <StackRefactorId>{}</StackRefactorId>\n <Status>CREATE_COMPLETE</Status>",
1051 xml_escape(&id),
1052 );
1053 Ok(xml_response("DescribeStackRefactor", inner, &rid))
1054 }
1055 "ExecuteStackRefactor" => {
1056 require_scalar(¶ms, "StackRefactorId")?;
1057 Ok(xml_response("ExecuteStackRefactor", String::new(), &rid))
1058 }
1059 "ListStackRefactors" => Ok(xml_response(
1060 "ListStackRefactors",
1061 " <StackRefactorSummaries/>".to_string(),
1062 &rid,
1063 )),
1064 "ListStackRefactorActions" => {
1065 require_scalar(¶ms, "StackRefactorId")?;
1066 Ok(xml_response(
1067 "ListStackRefactorActions",
1068 " <StackRefactorActions/>".to_string(),
1069 &rid,
1070 ))
1071 }
1072
1073 "ActivateType" => {
1075 let arn = Arn::new(
1076 "cloudformation",
1077 "us-east-1",
1078 &aid,
1079 &format!("type/resource/{}", rand_id()),
1080 )
1081 .to_string();
1082 Ok(xml_response(
1083 "ActivateType",
1084 format!(" <Arn>{}</Arn>", xml_escape(&arn)),
1085 &rid,
1086 ))
1087 }
1088 "DeactivateType" => Ok(xml_response("DeactivateType", String::new(), &rid)),
1089 "DescribeType" => {
1090 let arn = params.get("Arn").cloned().unwrap_or_else(|| {
1091 Arn::new("cloudformation", "us-east-1", &aid, "type/resource/Default")
1092 .to_string()
1093 });
1094 let inner = format!(
1095 " <Arn>{}</Arn>\n <Type>RESOURCE</Type>\n <TypeName>AWS::Custom::Type</TypeName>",
1096 xml_escape(&arn),
1097 );
1098 Ok(xml_response("DescribeType", inner, &rid))
1099 }
1100 "DescribeTypeRegistration" => {
1101 let token = params
1102 .get("RegistrationToken")
1103 .cloned()
1104 .ok_or_else(|| missing("RegistrationToken"))?;
1105 let inner = format!(
1106 " <ProgressStatus>COMPLETE</ProgressStatus>\n <Description>{}</Description>",
1107 xml_escape(&token),
1108 );
1109 Ok(xml_response("DescribeTypeRegistration", inner, &rid))
1110 }
1111 "RegisterType" => {
1112 require_scalar(¶ms, "TypeName")?;
1113 require_scalar(¶ms, "SchemaHandlerPackage")?;
1114 let token = rand_id();
1115 Ok(xml_response(
1116 "RegisterType",
1117 format!(
1118 " <RegistrationToken>{}</RegistrationToken>",
1119 xml_escape(&token)
1120 ),
1121 &rid,
1122 ))
1123 }
1124 "DeregisterType" => Ok(xml_response("DeregisterType", String::new(), &rid)),
1125 "ListTypes" => Ok(xml_response(
1126 "ListTypes",
1127 " <TypeSummaries/>".to_string(),
1128 &rid,
1129 )),
1130 "ListTypeRegistrations" => Ok(xml_response(
1131 "ListTypeRegistrations",
1132 " <RegistrationTokenList/>".to_string(),
1133 &rid,
1134 )),
1135 "ListTypeVersions" => Ok(xml_response(
1136 "ListTypeVersions",
1137 " <TypeVersionSummaries/>".to_string(),
1138 &rid,
1139 )),
1140 "BatchDescribeTypeConfigurations" => {
1141 Ok(xml_response(
1145 "BatchDescribeTypeConfigurations",
1146 " <Errors/>\n <TypeConfigurations/>".to_string(),
1147 &rid,
1148 ))
1149 }
1150 "SetTypeConfiguration" => {
1151 require_scalar(¶ms, "Configuration")?;
1152 let arn = Arn::new(
1153 "cloudformation",
1154 "us-east-1",
1155 &aid,
1156 &format!("type-config/{}", rand_id()),
1157 )
1158 .to_string();
1159 Ok(xml_response(
1160 "SetTypeConfiguration",
1161 format!(
1162 " <ConfigurationArn>{}</ConfigurationArn>",
1163 xml_escape(&arn)
1164 ),
1165 &rid,
1166 ))
1167 }
1168 "SetTypeDefaultVersion" => {
1169 Ok(xml_response("SetTypeDefaultVersion", String::new(), &rid))
1170 }
1171 "TestType" => {
1172 let arn = Arn::new(
1173 "cloudformation",
1174 "us-east-1",
1175 &aid,
1176 &format!("type/resource/{}", rand_id()),
1177 )
1178 .to_string();
1179 Ok(xml_response(
1180 "TestType",
1181 format!(" <TypeVersionArn>{}</TypeVersionArn>", xml_escape(&arn)),
1182 &rid,
1183 ))
1184 }
1185 "PublishType" => {
1186 let arn = Arn::new(
1187 "cloudformation",
1188 "us-east-1",
1189 &aid,
1190 &format!("type/resource/{}", rand_id()),
1191 )
1192 .to_string();
1193 Ok(xml_response(
1194 "PublishType",
1195 format!(" <PublicTypeArn>{}</PublicTypeArn>", xml_escape(&arn)),
1196 &rid,
1197 ))
1198 }
1199 "RegisterPublisher" => {
1200 let id = rand_id();
1201 Ok(xml_response(
1202 "RegisterPublisher",
1203 format!(" <PublisherId>{}</PublisherId>", xml_escape(&id)),
1204 &rid,
1205 ))
1206 }
1207 "DescribePublisher" => {
1208 let id = params
1209 .get("PublisherId")
1210 .cloned()
1211 .unwrap_or_else(|| "default-publisher".to_string());
1212 let inner = format!(
1213 " <PublisherId>{}</PublisherId>\n <PublisherStatus>VERIFIED</PublisherStatus>\n <IdentityProvider>AWS_Marketplace</IdentityProvider>",
1214 xml_escape(&id),
1215 );
1216 Ok(xml_response("DescribePublisher", inner, &rid))
1217 }
1218
1219 "CreateGeneratedTemplate" => {
1221 let name = params
1222 .get("GeneratedTemplateName")
1223 .ok_or_else(|| missing("GeneratedTemplateName"))?
1224 .clone();
1225 let id = Arn::new(
1226 "cloudformation",
1227 "us-east-1",
1228 &aid,
1229 &format!("generatedtemplate/{}", rand_id()),
1230 )
1231 .to_string();
1232 let entry = json!({"GeneratedTemplateId": id.clone(), "Name": name.clone(), "Status": "COMPLETE"});
1233 let mut accounts = self.state.write();
1234 let state = accounts.get_or_create(&aid);
1235 store(&mut state.extras, "generated_templates").insert(name.clone(), entry);
1236 Ok(xml_response(
1237 "CreateGeneratedTemplate",
1238 format!(
1239 " <GeneratedTemplateId>{}</GeneratedTemplateId>",
1240 xml_escape(&id)
1241 ),
1242 &rid,
1243 ))
1244 }
1245 "UpdateGeneratedTemplate" => {
1246 let name = params
1247 .get("GeneratedTemplateName")
1248 .ok_or_else(|| missing("GeneratedTemplateName"))?
1249 .clone();
1250 let id = Arn::new(
1251 "cloudformation",
1252 "us-east-1",
1253 &aid,
1254 &format!("generatedtemplate/{name}"),
1255 )
1256 .to_string();
1257 Ok(xml_response(
1258 "UpdateGeneratedTemplate",
1259 format!(
1260 " <GeneratedTemplateId>{}</GeneratedTemplateId>",
1261 xml_escape(&id)
1262 ),
1263 &rid,
1264 ))
1265 }
1266 "DescribeGeneratedTemplate" => {
1267 let name = params
1268 .get("GeneratedTemplateName")
1269 .ok_or_else(|| missing("GeneratedTemplateName"))?
1270 .clone();
1271 let inner = format!(
1272 " <GeneratedTemplateId>arn:aws:cloudformation:us-east-1:{}:generatedtemplate/{}</GeneratedTemplateId>\n <GeneratedTemplateName>{}</GeneratedTemplateName>\n <Status>COMPLETE</Status>",
1273 xml_escape(&aid),
1274 xml_escape(&name),
1275 xml_escape(&name),
1276 );
1277 Ok(xml_response("DescribeGeneratedTemplate", inner, &rid))
1278 }
1279 "GetGeneratedTemplate" => {
1280 require_scalar(¶ms, "GeneratedTemplateName")?;
1281 Ok(xml_response(
1282 "GetGeneratedTemplate",
1283 " <Status>COMPLETE</Status>\n <TemplateBody>{}</TemplateBody>"
1284 .to_string(),
1285 &rid,
1286 ))
1287 }
1288 "DeleteGeneratedTemplate" => {
1289 let name = params
1290 .get("GeneratedTemplateName")
1291 .ok_or_else(|| missing("GeneratedTemplateName"))?
1292 .clone();
1293 let mut accounts = self.state.write();
1294 let state = accounts.get_or_create(&aid);
1295 if let Some(m) = state.extras.get_mut("generated_templates") {
1296 m.remove(&name);
1297 }
1298 Ok(xml_response("DeleteGeneratedTemplate", String::new(), &rid))
1299 }
1300 "ListGeneratedTemplates" => Ok(xml_response(
1301 "ListGeneratedTemplates",
1302 " <Summaries/>".to_string(),
1303 &rid,
1304 )),
1305
1306 "StartResourceScan" => {
1308 let id = Arn::new(
1309 "cloudformation",
1310 "us-east-1",
1311 &aid,
1312 &format!("resourceScan/{}", rand_id()),
1313 )
1314 .to_string();
1315 Ok(xml_response(
1316 "StartResourceScan",
1317 format!(" <ResourceScanId>{}</ResourceScanId>", xml_escape(&id)),
1318 &rid,
1319 ))
1320 }
1321 "DescribeResourceScan" => {
1322 let id = params
1323 .get("ResourceScanId")
1324 .cloned()
1325 .ok_or_else(|| missing("ResourceScanId"))?;
1326 let inner = format!(
1327 " <ResourceScanId>{}</ResourceScanId>\n <Status>COMPLETE</Status>",
1328 xml_escape(&id),
1329 );
1330 Ok(xml_response("DescribeResourceScan", inner, &rid))
1331 }
1332 "ListResourceScans" => Ok(xml_response(
1333 "ListResourceScans",
1334 " <ResourceScanSummaries/>".to_string(),
1335 &rid,
1336 )),
1337 "ListResourceScanResources" => {
1338 require_scalar(¶ms, "ResourceScanId")?;
1339 Ok(xml_response(
1340 "ListResourceScanResources",
1341 " <Resources/>".to_string(),
1342 &rid,
1343 ))
1344 }
1345 "ListResourceScanRelatedResources" => {
1346 require_scalar(¶ms, "ResourceScanId")?;
1347 Ok(xml_response(
1348 "ListResourceScanRelatedResources",
1349 " <RelatedResources/>".to_string(),
1350 &rid,
1351 ))
1352 }
1353
1354 "DetectStackDrift" => {
1356 let stack_name = params
1357 .get("StackName")
1358 .ok_or_else(|| missing("StackName"))?
1359 .clone();
1360 let id = rand_id();
1361
1362 let resources: Vec<StackResource> = {
1363 let accounts = self.state.read();
1364 let stack = accounts.get(&aid).and_then(|s| {
1365 s.stacks.values().find(|st| {
1366 (st.name == stack_name || st.stack_id == stack_name)
1367 && st.status != "DELETE_COMPLETE"
1368 })
1369 });
1370 stack.map(|s| s.resources.clone()).unwrap_or_default()
1371 };
1372
1373 let mut drifted_resources: Vec<Value> = Vec::new();
1374
1375 for resource in &resources {
1376 let exists = match resource.resource_type.as_str() {
1377 "AWS::SQS::Queue" => self
1378 .deps
1379 .sqs
1380 .read()
1381 .get(&aid)
1382 .map(|s| s.queues.contains_key(&resource.physical_id))
1383 .unwrap_or(false),
1384 "AWS::SNS::Topic" => self
1385 .deps
1386 .sns
1387 .read()
1388 .get(&aid)
1389 .map(|s| s.topics.contains_key(&resource.physical_id))
1390 .unwrap_or(false),
1391 "AWS::S3::Bucket" => self
1392 .deps
1393 .s3
1394 .read()
1395 .get(&aid)
1396 .map(|s| s.buckets.contains_key(&resource.physical_id))
1397 .unwrap_or(false),
1398 "AWS::Lambda::Function" => self
1399 .deps
1400 .lambda
1401 .read()
1402 .get(&aid)
1403 .map(|s| s.functions.contains_key(&resource.physical_id))
1404 .unwrap_or(false),
1405 "AWS::IAM::Role" => self
1406 .deps
1407 .iam
1408 .read()
1409 .get(&aid)
1410 .map(|s| s.roles.contains_key(&resource.physical_id))
1411 .unwrap_or(false),
1412 "AWS::DynamoDB::Table" => self
1413 .deps
1414 .dynamodb
1415 .read()
1416 .get(&aid)
1417 .map(|s| s.tables.values().any(|t| t.arn == resource.physical_id))
1418 .unwrap_or(false),
1419 "AWS::KMS::Key" => self
1420 .deps
1421 .kms
1422 .read()
1423 .get(&aid)
1424 .map(|s| s.keys.contains_key(&resource.physical_id))
1425 .unwrap_or(false),
1426 "AWS::SecretsManager::Secret" => self
1427 .deps
1428 .secretsmanager
1429 .read()
1430 .get(&aid)
1431 .map(|s| s.secrets.contains_key(&resource.physical_id))
1432 .unwrap_or(false),
1433 _ => true, };
1435 if !exists {
1436 drifted_resources.push(json!({
1437 "LogicalResourceId": resource.logical_id,
1438 "PhysicalResourceId": resource.physical_id,
1439 "ResourceType": resource.resource_type,
1440 "StackResourceDriftStatus": "DELETED",
1441 "PropertyDifferences": [],
1442 }));
1443 }
1444 }
1445
1446 let stack_drift_status = if drifted_resources.is_empty() {
1447 "IN_SYNC"
1448 } else {
1449 "DRIFTED"
1450 };
1451
1452 let record = json!({
1453 "StackDriftDetectionId": id,
1454 "StackName": stack_name,
1455 "StackDriftStatus": stack_drift_status,
1456 "DetectionStatus": "DETECTION_COMPLETE",
1457 "DriftedResources": drifted_resources,
1458 });
1459
1460 {
1461 let mut accounts = self.state.write();
1462 let state = accounts.get_or_create(&aid);
1463 store(&mut state.extras, "drift_detection").insert(id.clone(), record);
1464 }
1465
1466 Ok(xml_response(
1467 "DetectStackDrift",
1468 format!(
1469 " <StackDriftDetectionId>{}</StackDriftDetectionId>",
1470 xml_escape(&id)
1471 ),
1472 &rid,
1473 ))
1474 }
1475 "DetectStackResourceDrift" => {
1476 let stack_name = params
1477 .get("StackName")
1478 .ok_or_else(|| missing("StackName"))?
1479 .clone();
1480 let logical = params
1481 .get("LogicalResourceId")
1482 .ok_or_else(|| missing("LogicalResourceId"))?
1483 .clone();
1484 let accounts = self.state.read();
1485 let resource_drift = accounts
1486 .get(&aid)
1487 .and_then(|s| {
1488 s.stacks.values().find(|st| {
1489 (st.name == stack_name || st.stack_id == stack_name)
1490 && st.status != "DELETE_COMPLETE"
1491 })
1492 })
1493 .and_then(|stack| stack.resources.iter().find(|r| r.logical_id == logical))
1494 .map(|resource| {
1495 let exists = match resource.resource_type.as_str() {
1496 "AWS::SQS::Queue" => self
1497 .deps
1498 .sqs
1499 .read()
1500 .get(&aid)
1501 .map(|s| s.queues.contains_key(&resource.physical_id))
1502 .unwrap_or(false),
1503 "AWS::SNS::Topic" => self
1504 .deps
1505 .sns
1506 .read()
1507 .get(&aid)
1508 .map(|s| s.topics.contains_key(&resource.physical_id))
1509 .unwrap_or(false),
1510 "AWS::S3::Bucket" => self
1511 .deps
1512 .s3
1513 .read()
1514 .get(&aid)
1515 .map(|s| s.buckets.contains_key(&resource.physical_id))
1516 .unwrap_or(false),
1517 "AWS::Lambda::Function" => self
1518 .deps
1519 .lambda
1520 .read()
1521 .get(&aid)
1522 .map(|s| s.functions.contains_key(&resource.physical_id))
1523 .unwrap_or(false),
1524 "AWS::IAM::Role" => self
1525 .deps
1526 .iam
1527 .read()
1528 .get(&aid)
1529 .map(|s| s.roles.contains_key(&resource.physical_id))
1530 .unwrap_or(false),
1531 "AWS::DynamoDB::Table" => self
1532 .deps
1533 .dynamodb
1534 .read()
1535 .get(&aid)
1536 .map(|s| s.tables.values().any(|t| t.arn == resource.physical_id))
1537 .unwrap_or(false),
1538 "AWS::KMS::Key" => self
1539 .deps
1540 .kms
1541 .read()
1542 .get(&aid)
1543 .map(|s| s.keys.contains_key(&resource.physical_id))
1544 .unwrap_or(false),
1545 "AWS::SecretsManager::Secret" => self
1546 .deps
1547 .secretsmanager
1548 .read()
1549 .get(&aid)
1550 .map(|s| s.secrets.contains_key(&resource.physical_id))
1551 .unwrap_or(false),
1552 _ => true,
1553 };
1554 if exists {
1555 "IN_SYNC"
1556 } else {
1557 "DELETED"
1558 }
1559 })
1560 .unwrap_or("NOT_CHECKED");
1561
1562 let inner = format!(
1563 " <StackResourceDrift>\n <LogicalResourceId>{}</LogicalResourceId>\n <StackResourceDriftStatus>{}</StackResourceDriftStatus>\n </StackResourceDrift>",
1564 xml_escape(&logical),
1565 xml_escape(resource_drift),
1566 );
1567 Ok(xml_response("DetectStackResourceDrift", inner, &rid))
1568 }
1569 "DetectStackSetDrift" => {
1570 require_scalar(¶ms, "StackSetName")?;
1571 let op_id = rand_id();
1572 Ok(xml_response(
1573 "DetectStackSetDrift",
1574 format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
1575 &rid,
1576 ))
1577 }
1578 "DescribeStackDriftDetectionStatus" => {
1579 let id = params
1580 .get("StackDriftDetectionId")
1581 .ok_or_else(|| missing("StackDriftDetectionId"))?
1582 .clone();
1583 let accounts = self.state.read();
1584 let record = accounts
1585 .get(&aid)
1586 .and_then(|s| s.extras.get("drift_detection"))
1587 .and_then(|m| m.get(&id))
1588 .cloned()
1589 .unwrap_or_else(|| {
1590 json!({
1591 "StackDriftDetectionId": id,
1592 "StackDriftStatus": "IN_SYNC",
1593 "DetectionStatus": "DETECTION_COMPLETE",
1594 })
1595 });
1596 let stack_id = record["StackId"]
1602 .as_str()
1603 .map(str::to_owned)
1604 .unwrap_or_else(|| {
1605 Arn::new(
1606 "cloudformation",
1607 "us-east-1",
1608 &aid,
1609 &format!("stack/drift-{id}/{}", rand_id()),
1610 )
1611 .to_string()
1612 });
1613 let timestamp = record["Timestamp"]
1614 .as_str()
1615 .map(str::to_owned)
1616 .unwrap_or_else(|| "2024-01-01T00:00:00Z".to_string());
1617
1618 let inner = format!(
1619 " <StackId>{}</StackId>\n <StackDriftDetectionId>{}</StackDriftDetectionId>\n <DetectionStatus>{}</DetectionStatus>\n <StackDriftStatus>{}</StackDriftStatus>\n <Timestamp>{}</Timestamp>",
1620 xml_escape(&stack_id),
1621 xml_escape(record["StackDriftDetectionId"].as_str().unwrap_or("")),
1622 xml_escape(record["DetectionStatus"].as_str().unwrap_or("DETECTION_COMPLETE")),
1623 xml_escape(record["StackDriftStatus"].as_str().unwrap_or("IN_SYNC")),
1624 xml_escape(×tamp),
1625 );
1626 Ok(xml_response(
1627 "DescribeStackDriftDetectionStatus",
1628 inner,
1629 &rid,
1630 ))
1631 }
1632 "DescribeStackResourceDrifts" => {
1633 let stack_name = params
1634 .get("StackName")
1635 .cloned()
1636 .ok_or_else(|| missing("StackName"))?;
1637 let accounts = self.state.read();
1638 let drifted: Vec<Value> = accounts
1639 .get(&aid)
1640 .and_then(|s| {
1641 let found = s
1642 .stacks
1643 .values()
1644 .find(|st| {
1645 (st.name == stack_name || st.stack_id == stack_name)
1646 && st.status != "DELETE_COMPLETE"
1647 })
1648 .is_some();
1649 if !found {
1650 return None;
1651 }
1652 s.extras
1653 .get("drift_detection")
1654 .and_then(|m| {
1655 m.values()
1656 .find(|v| v["StackName"].as_str() == Some(stack_name.as_str()))
1657 })
1658 .and_then(|v| v["DriftedResources"].as_array().cloned())
1659 })
1660 .unwrap_or_default();
1661
1662 let inner = if drifted.is_empty() {
1663 " <StackResourceDrifts/>".to_string()
1664 } else {
1665 format!(
1666 " <StackResourceDrifts>\n{}\n </StackResourceDrifts>",
1667 members_xml(&drifted, |v| {
1668 format!(
1669 " <StackResourceDrift>\n <LogicalResourceId>{}</LogicalResourceId>\n <PhysicalResourceId>{}</PhysicalResourceId>\n <ResourceType>{}</ResourceType>\n <StackResourceDriftStatus>{}</StackResourceDriftStatus>\n </StackResourceDrift>",
1670 xml_escape(v["LogicalResourceId"].as_str().unwrap_or("")),
1671 xml_escape(v["PhysicalResourceId"].as_str().unwrap_or("")),
1672 xml_escape(v["ResourceType"].as_str().unwrap_or("")),
1673 xml_escape(v["StackResourceDriftStatus"].as_str().unwrap_or("IN_SYNC")),
1674 )
1675 }),
1676 )
1677 };
1678 Ok(xml_response("DescribeStackResourceDrifts", inner, &rid))
1679 }
1680 "DescribeStackResource" => {
1681 let stack_name = params
1682 .get("StackName")
1683 .ok_or_else(|| missing("StackName"))?
1684 .clone();
1685 let logical = params
1686 .get("LogicalResourceId")
1687 .ok_or_else(|| missing("LogicalResourceId"))?
1688 .clone();
1689 let accounts = self.state.read();
1690 let detail = accounts
1691 .get(&aid)
1692 .and_then(|s| s.stacks.get(&stack_name))
1693 .and_then(|s| s.resources.iter().find(|r| r.logical_id == logical))
1694 .map(|r| {
1695 (
1696 r.physical_id.clone(),
1697 r.resource_type.clone(),
1698 r.status.clone(),
1699 )
1700 })
1701 .unwrap_or_else(|| {
1702 (
1703 "pid".to_string(),
1704 "AWS::Custom".to_string(),
1705 "CREATE_COMPLETE".to_string(),
1706 )
1707 });
1708 let inner = format!(
1709 " <StackResourceDetail>\n <StackName>{}</StackName>\n <LogicalResourceId>{}</LogicalResourceId>\n <PhysicalResourceId>{}</PhysicalResourceId>\n <ResourceType>{}</ResourceType>\n <ResourceStatus>{}</ResourceStatus>\n <LastUpdatedTimestamp>{}</LastUpdatedTimestamp>\n </StackResourceDetail>",
1710 xml_escape(&stack_name),
1711 xml_escape(&logical),
1712 xml_escape(&detail.0),
1713 xml_escape(&detail.1),
1714 xml_escape(&detail.2),
1715 chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ"),
1716 );
1717 Ok(xml_response("DescribeStackResource", inner, &rid))
1718 }
1719
1720 "DescribeStackEvents" => {
1722 require_scalar(¶ms, "StackName")?;
1723 let stack_filter = params.get("StackName").cloned();
1724 let accounts = self.state.read();
1725 let events: Vec<Value> = accounts
1726 .get(&aid)
1727 .map(|s| {
1728 let mut all: Vec<Value> = Vec::new();
1729 for (sid, evs) in &s.events {
1730 let matches = match &stack_filter {
1732 None => true,
1733 Some(filter) => {
1734 sid == filter
1735 || s.stacks.values().any(|st| {
1736 st.stack_id == *sid
1737 && (st.name == *filter || st.stack_id == *filter)
1738 })
1739 }
1740 };
1741 if matches {
1742 all.extend(evs.iter().cloned());
1743 }
1744 }
1745 all.reverse();
1747 all
1748 })
1749 .unwrap_or_default();
1750 let inner = if events.is_empty() {
1751 " <StackEvents/>".to_string()
1752 } else {
1753 format!(
1754 " <StackEvents>\n{}\n </StackEvents>",
1755 members_xml(&events, |v| {
1756 format!(
1757 " <EventId>{}</EventId>\n <StackId>{}</StackId>\n <StackName>{}</StackName>\n <LogicalResourceId>{}</LogicalResourceId>\n <PhysicalResourceId>{}</PhysicalResourceId>\n <ResourceType>{}</ResourceType>\n <ResourceStatus>{}</ResourceStatus>\n <Timestamp>{}</Timestamp>",
1758 xml_escape(v["EventId"].as_str().unwrap_or("")),
1759 xml_escape(v["StackId"].as_str().unwrap_or("")),
1760 xml_escape(v["StackName"].as_str().unwrap_or("")),
1761 xml_escape(v["LogicalResourceId"].as_str().unwrap_or("")),
1762 xml_escape(v["PhysicalResourceId"].as_str().unwrap_or("")),
1763 xml_escape(v["ResourceType"].as_str().unwrap_or("")),
1764 xml_escape(v["ResourceStatus"].as_str().unwrap_or("")),
1765 xml_escape(v["Timestamp"].as_str().unwrap_or("")),
1766 )
1767 }),
1768 )
1769 };
1770 Ok(xml_response("DescribeStackEvents", inner, &rid))
1771 }
1772 "DescribeEvents" => Ok(xml_response(
1773 "DescribeEvents",
1774 " <Events/>".to_string(),
1775 &rid,
1776 )),
1777
1778 "GetHookResult" => Ok(xml_response(
1780 "GetHookResult",
1781 " <Status>HOOK_COMPLETE_SUCCEEDED</Status>".to_string(),
1782 &rid,
1783 )),
1784 "ListHookResults" => Ok(xml_response(
1785 "ListHookResults",
1786 " <HookResults/>".to_string(),
1787 &rid,
1788 )),
1789 "RecordHandlerProgress" => {
1790 require_scalar(¶ms, "BearerToken")?;
1791 require_scalar(¶ms, "OperationStatus")?;
1792 Ok(xml_response_no_result("RecordHandlerProgress", &rid))
1793 }
1794
1795 "ListExports" => {
1797 let accounts = self.state.read();
1798 let mut entries = String::new();
1799 if let Some(state) = accounts.get(&aid) {
1800 for (name, export) in &state.exports {
1801 entries.push_str(&format!(
1802 " <member>\n <ExportingStackId>{}</ExportingStackId>\n <Name>{}</Name>\n <Value>{}</Value>\n </member>\n",
1803 xml_escape(&export.exporting_stack_id),
1804 xml_escape(name),
1805 xml_escape(&export.value),
1806 ));
1807 }
1808 }
1809 let inner = if entries.is_empty() {
1810 " <Exports/>".to_string()
1811 } else {
1812 format!(" <Exports>\n{entries} </Exports>")
1813 };
1814 Ok(xml_response("ListExports", inner, &rid))
1815 }
1816 "ListImports" => {
1817 let export_name = params
1818 .get("ExportName")
1819 .cloned()
1820 .ok_or_else(|| missing("ExportName"))?;
1821 let accounts = self.state.read();
1822 let mut entries = String::new();
1823 if let Some(state) = accounts.get(&aid) {
1824 if let Some(consumers) = state.imports.get(&export_name) {
1825 for stack_name in consumers {
1826 entries.push_str(&format!(
1827 " <member>{}</member>\n",
1828 xml_escape(stack_name)
1829 ));
1830 }
1831 }
1832 }
1833 let inner = if entries.is_empty() {
1834 " <Imports/>".to_string()
1835 } else {
1836 format!(" <Imports>\n{entries} </Imports>")
1837 };
1838 Ok(xml_response("ListImports", inner, &rid))
1839 }
1840
1841 "GetStackPolicy" => {
1843 let stack = params
1844 .get("StackName")
1845 .ok_or_else(|| missing("StackName"))?
1846 .clone();
1847 let accounts = self.state.read();
1848 let body = accounts.get(&aid)
1849 .and_then(|s| s.stack_policies.get(&stack))
1850 .cloned()
1851 .unwrap_or_else(|| r#"{"Statement":[{"Effect":"Allow","Action":"Update:*","Principal":"*","Resource":"*"}]}"#.to_string());
1852 let inner = format!(
1853 " <StackPolicyBody>{}</StackPolicyBody>",
1854 xml_escape(&body)
1855 );
1856 Ok(xml_response("GetStackPolicy", inner, &rid))
1857 }
1858 "SetStackPolicy" => {
1859 let stack = params
1860 .get("StackName")
1861 .ok_or_else(|| missing("StackName"))?
1862 .clone();
1863 let body = params.get("StackPolicyBody").cloned().unwrap_or_default();
1864 let mut accounts = self.state.write();
1865 let state = accounts.get_or_create(&aid);
1866 state.stack_policies.insert(stack, body);
1867 Ok(xml_response_no_result("SetStackPolicy", &rid))
1868 }
1869
1870 "UpdateTerminationProtection" => {
1872 let stack = params
1873 .get("StackName")
1874 .ok_or_else(|| missing("StackName"))?
1875 .clone();
1876 let enabled_raw = params
1877 .get("EnableTerminationProtection")
1878 .ok_or_else(|| missing("EnableTerminationProtection"))?;
1879 let enabled = enabled_raw.eq_ignore_ascii_case("true");
1880 let stack_id = {
1881 let mut accounts = self.state.write();
1882 let state = accounts.get_or_create(&aid);
1883 state.termination_protection.insert(stack.clone(), enabled);
1884 state
1885 .stacks
1886 .get(&stack)
1887 .map(|s| s.stack_id.clone())
1888 .unwrap_or_else(|| stack.clone())
1889 };
1890 Ok(xml_response(
1891 "UpdateTerminationProtection",
1892 format!(" <StackId>{}</StackId>", xml_escape(&stack_id)),
1893 &rid,
1894 ))
1895 }
1896
1897 "DescribeAccountLimits" => Ok(xml_response(
1899 "DescribeAccountLimits",
1900 r#" <AccountLimits>
1901 <member>
1902 <Name>StackLimit</Name>
1903 <Value>2000</Value>
1904 </member>
1905 </AccountLimits>"#
1906 .to_string(),
1907 &rid,
1908 )),
1909 "ActivateOrganizationsAccess" => {
1910 let mut accounts = self.state.write();
1911 let state = accounts.get_or_create(&aid);
1912 state.orgs_access_enabled = true;
1913 Ok(xml_response(
1914 "ActivateOrganizationsAccess",
1915 String::new(),
1916 &rid,
1917 ))
1918 }
1919 "DeactivateOrganizationsAccess" => {
1920 let mut accounts = self.state.write();
1921 let state = accounts.get_or_create(&aid);
1922 state.orgs_access_enabled = false;
1923 Ok(xml_response(
1924 "DeactivateOrganizationsAccess",
1925 String::new(),
1926 &rid,
1927 ))
1928 }
1929 "DescribeOrganizationsAccess" => {
1930 let accounts = self.state.read();
1931 let status = if accounts
1932 .get(&aid)
1933 .map(|s| s.orgs_access_enabled)
1934 .unwrap_or(false)
1935 {
1936 "ENABLED"
1937 } else {
1938 "DISABLED"
1939 };
1940 Ok(xml_response(
1941 "DescribeOrganizationsAccess",
1942 format!(" <Status>{}</Status>", status),
1943 &rid,
1944 ))
1945 }
1946 "ValidateTemplate" => Ok(xml_response(
1947 "ValidateTemplate",
1948 " <Description>Validated</Description>\n <Capabilities/>\n <Parameters/>"
1949 .to_string(),
1950 &rid,
1951 )),
1952 "EstimateTemplateCost" => Ok(xml_response(
1953 "EstimateTemplateCost",
1954 " <Url>https://calculator.aws/#/estimate</Url>".to_string(),
1955 &rid,
1956 )),
1957 "GetTemplateSummary" => Ok(xml_response(
1958 "GetTemplateSummary",
1959 " <Parameters/>\n <ResourceTypes/>\n <Capabilities/>".to_string(),
1960 &rid,
1961 )),
1962 "CancelUpdateStack" => {
1963 params
1964 .get("StackName")
1965 .ok_or_else(|| missing("StackName"))?;
1966 Ok(xml_response_no_result("CancelUpdateStack", &rid))
1967 }
1968 "ContinueUpdateRollback" => {
1969 params
1970 .get("StackName")
1971 .ok_or_else(|| missing("StackName"))?;
1972 Ok(xml_response("ContinueUpdateRollback", String::new(), &rid))
1973 }
1974 "RollbackStack" => {
1975 let stack = params
1976 .get("StackName")
1977 .ok_or_else(|| missing("StackName"))?
1978 .clone();
1979 let stack_id = {
1980 let accounts = self.state.read();
1981 accounts
1982 .get(&aid)
1983 .and_then(|s| s.stacks.get(&stack))
1984 .map(|s| s.stack_id.clone())
1985 .unwrap_or_else(|| stack.clone())
1986 };
1987 Ok(xml_response(
1988 "RollbackStack",
1989 format!(" <StackId>{}</StackId>", xml_escape(&stack_id)),
1990 &rid,
1991 ))
1992 }
1993 "SignalResource" => {
1994 require_scalar(¶ms, "StackName")?;
1995 require_scalar(¶ms, "LogicalResourceId")?;
1996 require_scalar(¶ms, "UniqueId")?;
1997 require_scalar(¶ms, "Status")?;
1998 Ok(xml_response_no_result("SignalResource", &rid))
1999 }
2000
2001 _ => Err(AwsServiceError::action_not_implemented(
2002 "cloudformation",
2003 &action,
2004 )),
2005 }
2006 }
2007}
2008
2009#[cfg(test)]
2010mod tests {
2011 use super::parse_s3_url;
2012 use crate::service::{CloudFormationDeps, CloudFormationService};
2013 use crate::state::{CloudFormationState, SharedCloudFormationState};
2014 use fakecloud_core::delivery::DeliveryBus;
2015 use fakecloud_core::multi_account::MultiAccountState;
2016 use fakecloud_core::service::AwsRequest;
2017 use http::Method;
2018 use parking_lot::RwLock;
2019 use std::collections::HashMap;
2020 use std::sync::Arc;
2021
2022 #[test]
2023 fn parse_s3_url_handles_path_and_virtual_hosted() {
2024 assert_eq!(
2027 parse_s3_url("http://127.0.0.1:4566/bucket/deploy/template.json"),
2028 Some(("bucket".to_string(), "deploy/template.json".to_string()))
2029 );
2030 assert_eq!(
2032 parse_s3_url("https://s3.us-east-1.amazonaws.com/my-bucket/key.yaml"),
2033 Some(("my-bucket".to_string(), "key.yaml".to_string()))
2034 );
2035 assert_eq!(
2037 parse_s3_url("https://my-bucket.s3.amazonaws.com/key.yaml"),
2038 Some(("my-bucket".to_string(), "key.yaml".to_string()))
2039 );
2040 assert_eq!(
2042 parse_s3_url("https://s3.amazonaws.com/b/k.json?versionId=abc"),
2043 Some(("b".to_string(), "k.json".to_string()))
2044 );
2045 assert_eq!(parse_s3_url("https://s3.amazonaws.com/bucket-only"), None);
2047 }
2048
2049 fn deps() -> CloudFormationDeps {
2050 use fakecloud_dynamodb::DynamoDbState;
2051 use fakecloud_ecr::EcrState;
2052 use fakecloud_eventbridge::EventBridgeState;
2053 use fakecloud_iam::IamState;
2054 use fakecloud_kinesis::KinesisState;
2055 use fakecloud_kms::KmsState;
2056 use fakecloud_lambda::LambdaState;
2057 use fakecloud_logs::LogsState;
2058 use fakecloud_s3::S3State;
2059 use fakecloud_secretsmanager::SecretsManagerState;
2060 use fakecloud_sns::SnsState;
2061 use fakecloud_sqs::SqsState;
2062 use fakecloud_ssm::SsmState;
2063
2064 fn shared<T: fakecloud_core::multi_account::AccountState>(
2065 ) -> Arc<RwLock<MultiAccountState<T>>> {
2066 Arc::new(RwLock::new(MultiAccountState::<T>::new(
2067 "000000000000",
2068 "us-east-1",
2069 "",
2070 )))
2071 }
2072 CloudFormationDeps {
2073 sqs: shared::<SqsState>(),
2074 sns: shared::<SnsState>(),
2075 ssm: shared::<SsmState>(),
2076 iam: shared::<IamState>(),
2077 s3: shared::<S3State>(),
2078 eventbridge: shared::<EventBridgeState>(),
2079 dynamodb: shared::<DynamoDbState>(),
2080 logs: shared::<LogsState>(),
2081 lambda: shared::<LambdaState>(),
2082 secretsmanager: shared::<SecretsManagerState>(),
2083 kinesis: shared::<KinesisState>(),
2084 kms: shared::<KmsState>(),
2085 ecr: shared::<EcrState>(),
2086 cloudwatch: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
2087 elbv2: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
2088 organizations: Arc::new(RwLock::new(None)),
2089 cognito: shared::<fakecloud_cognito::CognitoState>(),
2090 rds: shared::<fakecloud_rds::RdsState>(),
2091 ecs: shared::<fakecloud_ecs::EcsState>(),
2092 acm: Arc::new(RwLock::new(fakecloud_acm::AcmAccounts::new())),
2093 elasticache: shared::<fakecloud_elasticache::ElastiCacheState>(),
2094 route53: Arc::new(RwLock::new(fakecloud_route53::Route53Accounts::new())),
2095 cloudfront: Arc::new(RwLock::new(fakecloud_cloudfront::CloudFrontAccounts::new())),
2096 stepfunctions: shared::<fakecloud_stepfunctions::StepFunctionsState>(),
2097 wafv2: Arc::new(RwLock::new(fakecloud_wafv2::Wafv2Accounts::default())),
2098 apigateway: shared::<fakecloud_apigateway::ApiGatewayState>(),
2099 apigatewayv2: shared::<fakecloud_apigatewayv2::ApiGatewayV2State>(),
2100 ses: shared::<fakecloud_ses::SesState>(),
2101 application_autoscaling: Arc::new(parking_lot::RwLock::new(
2102 fakecloud_application_autoscaling::ApplicationAutoScalingAccounts::new(),
2103 )),
2104 athena: Arc::new(parking_lot::RwLock::new(
2105 fakecloud_athena::AthenaAccounts::new(),
2106 )),
2107 firehose: Arc::new(parking_lot::RwLock::new(
2108 fakecloud_firehose::FirehoseAccounts::new(),
2109 )),
2110 glue: Arc::new(parking_lot::RwLock::new(fakecloud_glue::GlueAccounts::new())),
2111 delivery: Arc::new(DeliveryBus::new()),
2112 lambda_runtime: None,
2113 }
2114 }
2115
2116 fn svc() -> CloudFormationService {
2117 let state: SharedCloudFormationState =
2118 Arc::new(RwLock::new(MultiAccountState::<CloudFormationState>::new(
2119 "000000000000",
2120 "us-east-1",
2121 "",
2122 )));
2123 CloudFormationService::new(state, deps())
2124 }
2125
2126 fn req(action: &str, params: &[(&str, &str)]) -> AwsRequest {
2127 let mut q = HashMap::new();
2128 q.insert("Action".to_string(), action.to_string());
2129 for (k, v) in params {
2130 q.insert(k.to_string(), v.to_string());
2131 }
2132 AwsRequest {
2133 service: "cloudformation".to_string(),
2134 method: Method::POST,
2135 raw_path: "/".to_string(),
2136 raw_query: String::new(),
2137 path_segments: vec![],
2138 query_params: q,
2139 headers: http::HeaderMap::new(),
2140 body: bytes::Bytes::new(),
2141 body_stream: parking_lot::Mutex::new(None),
2142 account_id: "000000000000".to_string(),
2143 region: "us-east-1".to_string(),
2144 request_id: "rid".to_string(),
2145 action: action.to_string(),
2146 is_query_protocol: true,
2147 access_key_id: None,
2148 principal: None,
2149 }
2150 }
2151
2152 fn ok(action: &str, params: &[(&str, &str)]) {
2153 let r = svc().handle_extra_action(&req(action, params));
2154 match r {
2155 Ok(resp) => assert!(resp.status.is_success(), "{action} status: {}", resp.status),
2156 Err(e) => panic!("{action} failed: {e:?}"),
2157 }
2158 }
2159
2160 #[test]
2161 fn change_sets() {
2162 ok(
2163 "CreateChangeSet",
2164 &[("StackName", "s"), ("ChangeSetName", "cs")],
2165 );
2166 ok("DescribeChangeSet", &[("ChangeSetName", "cs")]);
2167 ok("DescribeChangeSetHooks", &[("ChangeSetName", "cs")]);
2168 ok("ListChangeSets", &[("StackName", "s")]);
2169 ok("ExecuteChangeSet", &[("ChangeSetName", "cs")]);
2170 ok("DeleteChangeSet", &[("ChangeSetName", "cs")]);
2171 }
2172
2173 #[test]
2174 fn stack_sets_instances_refactors() {
2175 ok("CreateStackSet", &[("StackSetName", "ss")]);
2176 ok("DescribeStackSet", &[("StackSetName", "ss")]);
2177 ok("ListStackSets", &[]);
2178 ok("UpdateStackSet", &[("StackSetName", "ss")]);
2179 ok(
2180 "DescribeStackSetOperation",
2181 &[("StackSetName", "ss"), ("OperationId", "op")],
2182 );
2183 ok("ListStackSetOperations", &[("StackSetName", "ss")]);
2184 ok(
2185 "ListStackSetOperationResults",
2186 &[("StackSetName", "ss"), ("OperationId", "op")],
2187 );
2188 ok(
2189 "ListStackSetAutoDeploymentTargets",
2190 &[("StackSetName", "ss")],
2191 );
2192 ok(
2193 "StopStackSetOperation",
2194 &[("StackSetName", "ss"), ("OperationId", "op")],
2195 );
2196 ok("ImportStacksToStackSet", &[("StackSetName", "ss")]);
2197 ok("DeleteStackSet", &[("StackSetName", "ss")]);
2198 ok(
2199 "CreateStackInstances",
2200 &[("StackSetName", "ss"), ("Regions.member.1", "us-east-1")],
2201 );
2202 ok(
2203 "UpdateStackInstances",
2204 &[("StackSetName", "ss"), ("Regions.member.1", "us-east-1")],
2205 );
2206 ok(
2207 "DeleteStackInstances",
2208 &[
2209 ("StackSetName", "ss"),
2210 ("Regions.member.1", "us-east-1"),
2211 ("RetainStacks", "false"),
2212 ],
2213 );
2214 ok(
2215 "DescribeStackInstance",
2216 &[
2217 ("StackSetName", "ss"),
2218 ("StackInstanceAccount", "000000000000"),
2219 ("StackInstanceRegion", "us-east-1"),
2220 ],
2221 );
2222 ok("ListStackInstances", &[("StackSetName", "ss")]);
2223 ok(
2224 "ListStackInstanceResourceDrifts",
2225 &[
2226 ("StackSetName", "ss"),
2227 ("StackInstanceAccount", "000000000000"),
2228 ("StackInstanceRegion", "us-east-1"),
2229 ("OperationId", "op"),
2230 ],
2231 );
2232 ok(
2233 "CreateStackRefactor",
2234 &[("StackDefinitions.member.1.StackName", "s")],
2235 );
2236 ok("DescribeStackRefactor", &[("StackRefactorId", "r")]);
2237 ok("ExecuteStackRefactor", &[("StackRefactorId", "r")]);
2238 ok("ListStackRefactors", &[]);
2239 ok("ListStackRefactorActions", &[("StackRefactorId", "r")]);
2240 }
2241
2242 #[test]
2243 fn types_and_publishers() {
2244 ok("ActivateType", &[]);
2245 ok("DeactivateType", &[]);
2246 ok("DescribeType", &[]);
2247 ok("DescribeTypeRegistration", &[("RegistrationToken", "tok")]);
2248 ok(
2249 "RegisterType",
2250 &[("TypeName", "T"), ("SchemaHandlerPackage", "pkg")],
2251 );
2252 ok("DeregisterType", &[]);
2253 ok("ListTypes", &[]);
2254 ok("ListTypeRegistrations", &[]);
2255 ok("ListTypeVersions", &[]);
2256 ok(
2257 "BatchDescribeTypeConfigurations",
2258 &[("TypeConfigurationIdentifiers.member.1.Type", "RESOURCE")],
2259 );
2260 ok("SetTypeConfiguration", &[("Configuration", "{}")]);
2261 ok("SetTypeDefaultVersion", &[]);
2262 ok("TestType", &[]);
2263 ok("PublishType", &[]);
2264 ok("RegisterPublisher", &[]);
2265 ok("DescribePublisher", &[]);
2266 }
2267
2268 #[test]
2269 fn templates_resource_scans_drift() {
2270 ok(
2271 "CreateGeneratedTemplate",
2272 &[("GeneratedTemplateName", "gt")],
2273 );
2274 ok(
2275 "UpdateGeneratedTemplate",
2276 &[("GeneratedTemplateName", "gt")],
2277 );
2278 ok(
2279 "DescribeGeneratedTemplate",
2280 &[("GeneratedTemplateName", "gt")],
2281 );
2282 ok("GetGeneratedTemplate", &[("GeneratedTemplateName", "gt")]);
2283 ok("ListGeneratedTemplates", &[]);
2284 ok(
2285 "DeleteGeneratedTemplate",
2286 &[("GeneratedTemplateName", "gt")],
2287 );
2288 ok("StartResourceScan", &[]);
2289 ok("DescribeResourceScan", &[("ResourceScanId", "rs")]);
2290 ok("ListResourceScans", &[]);
2291 ok("ListResourceScanResources", &[("ResourceScanId", "rs")]);
2292 ok(
2293 "ListResourceScanRelatedResources",
2294 &[
2295 ("ResourceScanId", "rs"),
2296 ("Resources.member.1.ResourceType", "AWS::SQS::Queue"),
2297 ],
2298 );
2299 ok("DetectStackDrift", &[("StackName", "s")]);
2300 ok(
2301 "DetectStackResourceDrift",
2302 &[("StackName", "s"), ("LogicalResourceId", "L")],
2303 );
2304 ok("DetectStackSetDrift", &[("StackSetName", "ss")]);
2305 ok(
2306 "DescribeStackDriftDetectionStatus",
2307 &[("StackDriftDetectionId", "id")],
2308 );
2309 ok("DescribeStackResourceDrifts", &[("StackName", "s")]);
2310 ok(
2311 "DescribeStackResource",
2312 &[("StackName", "s"), ("LogicalResourceId", "L")],
2313 );
2314 }
2315
2316 #[test]
2317 fn events_hooks_imports_policies_org() {
2318 ok("DescribeStackEvents", &[("StackName", "s")]);
2319 ok("DescribeEvents", &[]);
2320 ok("GetHookResult", &[]);
2321 ok("ListHookResults", &[]);
2322 ok(
2323 "RecordHandlerProgress",
2324 &[("BearerToken", "tok"), ("OperationStatus", "SUCCESS")],
2325 );
2326 ok("ListExports", &[]);
2327 ok("ListImports", &[("ExportName", "SomeExport")]);
2328 ok("GetStackPolicy", &[("StackName", "s")]);
2329 ok("SetStackPolicy", &[("StackName", "s")]);
2330 ok(
2331 "UpdateTerminationProtection",
2332 &[("StackName", "s"), ("EnableTerminationProtection", "false")],
2333 );
2334 ok("DescribeAccountLimits", &[]);
2335 ok("ActivateOrganizationsAccess", &[]);
2336 ok("DescribeOrganizationsAccess", &[]);
2337 ok("DeactivateOrganizationsAccess", &[]);
2338 ok("ValidateTemplate", &[]);
2339 ok("EstimateTemplateCost", &[]);
2340 ok("GetTemplateSummary", &[]);
2341 ok("CancelUpdateStack", &[("StackName", "s")]);
2342 ok("ContinueUpdateRollback", &[("StackName", "s")]);
2343 ok("RollbackStack", &[("StackName", "s")]);
2344 ok(
2345 "SignalResource",
2346 &[
2347 ("StackName", "s"),
2348 ("LogicalResourceId", "L"),
2349 ("UniqueId", "U"),
2350 ("Status", "SUCCESS"),
2351 ],
2352 );
2353 }
2354}