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 is_hook_type(type_kind: Option<&str>, type_name: Option<&str>) -> bool {
93 if type_kind
94 .map(|k| k.eq_ignore_ascii_case("HOOK"))
95 .unwrap_or(false)
96 {
97 return true;
98 }
99 type_name
100 .map(|n| {
101 let upper = n.to_ascii_uppercase();
102 upper.ends_with("::HOOK") || upper.contains("::HOOK::")
103 })
104 .unwrap_or(false)
105}
106
107fn register_hook(
111 extras: &mut BTreeMap<String, BTreeMap<String, Value>>,
112 type_name: &str,
113 failure_mode: Option<&str>,
114 configuration: Option<&str>,
115) {
116 let entry = json!({
117 "TypeName": type_name,
118 "TypeVersionId": "00000001",
119 "TypeConfigurationVersionId": "1",
120 "FailureMode": failure_mode.unwrap_or("FAIL"),
124 "Configuration": configuration.unwrap_or(""),
125 });
126 store(extras, "hooks").insert(type_name.to_string(), entry);
127}
128
129struct HookTarget<'a> {
136 account_id: &'a str,
137 target_type: &'a str,
138 target_id: &'a str,
139 logical_resource_id: &'a str,
140 invocation_point: &'a str,
141 op_failed: bool,
142}
143
144fn record_hook_results(
145 extras: &mut BTreeMap<String, BTreeMap<String, Value>>,
146 hooks: &[Value],
147 target: &HookTarget<'_>,
148) {
149 let HookTarget {
150 account_id,
151 target_type,
152 target_id,
153 logical_resource_id,
154 invocation_point,
155 op_failed,
156 } = *target;
157 let now = Utc::now().timestamp_millis();
158 for hook in hooks {
159 let type_name = hook
160 .get("TypeName")
161 .and_then(Value::as_str)
162 .unwrap_or("Unknown::Hook");
163 let failure_mode = hook
164 .get("FailureMode")
165 .and_then(Value::as_str)
166 .unwrap_or("FAIL");
167 let status = if failure_mode.eq_ignore_ascii_case("FAIL") && op_failed {
171 "HOOK_COMPLETE_FAILED"
172 } else {
173 "HOOK_COMPLETE_SUCCEEDED"
174 };
175 let result_id = rand_id();
176 let type_arn = Arn::new(
177 "cloudformation",
178 "us-east-1",
179 account_id,
180 &format!("type/hook/{}", type_name.replace("::", "-")),
181 )
182 .to_string();
183 let record = json!({
184 "HookResultId": result_id,
185 "InvocationPoint": invocation_point,
186 "FailureMode": failure_mode,
187 "TypeName": type_name,
188 "TypeVersionId": hook.get("TypeVersionId").and_then(Value::as_str).unwrap_or("00000001"),
189 "TypeConfigurationVersionId": hook.get("TypeConfigurationVersionId").and_then(Value::as_str).unwrap_or("1"),
190 "TypeArn": type_arn,
191 "Status": status,
192 "HookStatusReason": if status == "HOOK_COMPLETE_FAILED" { "Hook failed" } else { "Hook succeeded" },
193 "InvokedAt": now,
194 "TargetType": target_type,
195 "TargetId": target_id,
196 "LogicalResourceId": logical_resource_id,
197 });
198 store(extras, "hook_results").insert(result_id, record);
199 }
200}
201
202fn missing(name: &str) -> AwsServiceError {
203 AwsServiceError::aws_error(
204 StatusCode::BAD_REQUEST,
205 "ValidationError",
206 format!("{name} is required"),
207 )
208}
209
210fn has_collection_param(params: &BTreeMap<String, String>, field: &str) -> bool {
216 let prefix = format!("{field}.");
217 params.keys().any(|k| k.starts_with(&prefix))
218}
219
220fn require_scalar(params: &BTreeMap<String, String>, field: &str) -> Result<(), AwsServiceError> {
225 if params.get(field).is_some() {
226 Ok(())
227 } else {
228 Err(missing(field))
229 }
230}
231
232fn require_collection(
233 params: &BTreeMap<String, String>,
234 field: &str,
235) -> Result<(), AwsServiceError> {
236 if has_collection_param(params, field) {
237 Ok(())
238 } else {
239 Err(missing(field))
240 }
241}
242
243fn parse_s3_url(url: &str) -> Option<(String, String)> {
249 let rest = url.split_once("://").map(|(_, r)| r).unwrap_or(url);
250 let (host, after) = rest.split_once('/')?;
251 let path = after.split(['?', '#']).next().unwrap_or(after);
252 if let Some(idx) = host.find(".s3") {
255 let bucket = &host[..idx];
256 if !bucket.is_empty() {
257 return Some((bucket.to_string(), path.to_string()));
258 }
259 }
260 let (bucket, key) = path.split_once('/')?;
262 if bucket.is_empty() || key.is_empty() {
263 return None;
264 }
265 Some((bucket.to_string(), key.to_string()))
266}
267
268impl CloudFormationService {
269 fn fetch_template_from_url(&self, account_id: &str, url: &str) -> Option<String> {
275 let (bucket, key) = parse_s3_url(url)?;
276 let mut accounts = self.deps.s3.write();
277 let state = accounts.get_or_create(account_id);
278 let body_ref = {
279 let b = state.buckets.get(&bucket)?;
280 b.objects.get(&key)?.body.clone()
281 };
282 let bytes = state.read_body(&body_ref).ok()?;
283 String::from_utf8(bytes.to_vec()).ok()
284 }
285
286 pub(crate) fn handle_extra_action(
287 &self,
288 req: &AwsRequest,
289 ) -> Result<AwsResponse, AwsServiceError> {
290 let action = req.action.clone();
291 let params = Self::get_all_params(req);
292 let aid = req.account_id.clone();
293 let rid = req.request_id.clone();
294
295 match action.as_str() {
296 "CreateChangeSet" => {
298 let stack_name = params
299 .get("StackName")
300 .ok_or_else(|| missing("StackName"))?
301 .clone();
302 let cs_name = params
303 .get("ChangeSetName")
304 .ok_or_else(|| missing("ChangeSetName"))?
305 .clone();
306 let template_body = {
313 let inline = params.get("TemplateBody").cloned().unwrap_or_default();
314 if inline.trim().is_empty() {
315 params
316 .get("TemplateURL")
317 .and_then(|url| self.fetch_template_from_url(&aid, url))
318 .unwrap_or(inline)
319 } else {
320 inline
321 }
322 };
323 let change_set_type = params
327 .get("ChangeSetType")
328 .map(|s| s.to_ascii_uppercase())
329 .unwrap_or_else(|| "UPDATE".to_string());
330
331 let mut cs_params = CloudFormationService::extract_parameters(¶ms);
332 CloudFormationService::merge_parameter_defaults(&mut cs_params, &template_body);
333 let cs_tags = CloudFormationService::extract_tags(¶ms);
334 let cs_notif = CloudFormationService::extract_notification_arns(¶ms);
335
336 let stack_lookup: Option<(String, Vec<crate::state::StackResource>)> = {
340 let accounts = self.state.read();
341 accounts.get(&aid).and_then(|s| {
342 s.stacks
343 .values()
344 .find(|st| {
345 (st.name == stack_name || st.stack_id == stack_name)
346 && st.status != "DELETE_COMPLETE"
347 })
348 .map(|st| (st.stack_id.clone(), st.resources.clone()))
349 })
350 };
351
352 let mut full_params: BTreeMap<String, String> = cs_params.clone();
355 full_params
356 .entry("AWS::Region".to_string())
357 .or_insert_with(|| req.region.clone());
358 full_params
359 .entry("AWS::AccountId".to_string())
360 .or_insert_with(|| aid.clone());
361 full_params
362 .entry("AWS::StackName".to_string())
363 .or_insert_with(|| stack_name.clone());
364 full_params
365 .entry("AWS::Partition".to_string())
366 .or_insert_with(|| "aws".to_string());
367 full_params
368 .entry("AWS::URLSuffix".to_string())
369 .or_insert_with(|| "amazonaws.com".to_string());
370 if let Some((sid, _)) = &stack_lookup {
371 full_params
372 .entry("AWS::StackId".to_string())
373 .or_insert_with(|| sid.clone());
374 }
375
376 let mut changes: Vec<Value> = Vec::new();
381 if !template_body.trim().is_empty() {
382 let parsed =
389 template::parse_template(&template_body, &full_params).unwrap_or_default();
390
391 let existing_resources = stack_lookup
392 .as_ref()
393 .map(|(_, r)| r.clone())
394 .unwrap_or_default();
395 let existing_by_id: BTreeMap<&str, &crate::state::StackResource> =
396 existing_resources
397 .iter()
398 .map(|r| (r.logical_id.as_str(), r))
399 .collect();
400 let new_by_id: BTreeMap<&str, &template::ResourceDefinition> = parsed
401 .resources
402 .iter()
403 .map(|r| (r.logical_id.as_str(), r))
404 .collect();
405
406 for r in &parsed.resources {
407 if let Some(existing) = existing_by_id.get(r.logical_id.as_str()) {
408 let replacement = if existing.resource_type != r.resource_type {
409 "True"
410 } else {
411 "Conditional"
412 };
413 changes.push(json!({
414 "Type": "Resource",
415 "ResourceChange": {
416 "Action": "Modify",
417 "LogicalResourceId": r.logical_id,
418 "PhysicalResourceId": existing.physical_id,
419 "ResourceType": r.resource_type,
420 "Replacement": replacement,
421 }
422 }));
423 } else {
424 changes.push(json!({
425 "Type": "Resource",
426 "ResourceChange": {
427 "Action": "Add",
428 "LogicalResourceId": r.logical_id,
429 "ResourceType": r.resource_type,
430 }
431 }));
432 }
433 }
434 for r in &existing_resources {
435 if !new_by_id.contains_key(r.logical_id.as_str()) {
436 changes.push(json!({
437 "Type": "Resource",
438 "ResourceChange": {
439 "Action": "Remove",
440 "LogicalResourceId": r.logical_id,
441 "PhysicalResourceId": r.physical_id,
442 "ResourceType": r.resource_type,
443 }
444 }));
445 }
446 }
447 }
448
449 let id = Arn::new(
450 "cloudformation",
451 "us-east-1",
452 &aid,
453 &format!("changeSet/{cs_name}/{}", rand_id()),
454 )
455 .to_string();
456 let stack_id_str = stack_lookup
457 .as_ref()
458 .map(|(s, _)| s.clone())
459 .unwrap_or_else(|| {
460 Arn::new(
461 "cloudformation",
462 "us-east-1",
463 &aid,
464 &format!("stack/{stack_name}/{}", rand_id()),
465 )
466 .to_string()
467 });
468
469 let activated_hooks: Vec<Value> = {
474 let accounts = self.state.read();
475 accounts
476 .get(&aid)
477 .and_then(|s| s.extras.get("hooks"))
478 .map(|m| m.values().cloned().collect())
479 .unwrap_or_default()
480 };
481
482 let entry = json!({
483 "Id": id,
484 "ChangeSetName": cs_name,
485 "StackId": stack_id_str,
486 "StackName": stack_name,
487 "Status": "CREATE_COMPLETE",
488 "ExecutionStatus": "AVAILABLE",
489 "TemplateBody": template_body,
490 "Parameters": cs_params,
491 "Tags": cs_tags,
492 "NotificationArns": cs_notif,
493 "Changes": changes,
494 "Hooks": activated_hooks,
495 });
496 let mut accounts = self.state.write();
497 let state = accounts.get_or_create(&aid);
498 if change_set_type == "CREATE" {
504 let live_status = state
510 .stacks
511 .values()
512 .find(|s| {
513 (s.name == stack_name || s.stack_id == stack_name)
514 && s.status != "DELETE_COMPLETE"
515 })
516 .map(|s| s.status.clone());
517 match live_status.as_deref() {
518 None => {
521 state.stacks.insert(
522 stack_name.clone(),
523 Stack {
524 name: stack_name.clone(),
525 stack_id: stack_id_str.clone(),
526 template: String::new(),
527 status: "REVIEW_IN_PROGRESS".to_string(),
528 resources: Vec::new(),
529 parameters: BTreeMap::new(),
530 tags: BTreeMap::new(),
531 created_at: Utc::now(),
532 updated_at: None,
533 description: None,
534 notification_arns: Vec::new(),
535 outputs: Vec::new(),
536 },
537 );
538 crate::service::record_stack_status_event(
547 state,
548 &stack_id_str,
549 &stack_name,
550 "AWS::CloudFormation::Stack",
551 "REVIEW_IN_PROGRESS",
552 );
553 }
554 Some("REVIEW_IN_PROGRESS") => {}
558 Some(_) => {
561 return Err(AwsServiceError::aws_error(
562 StatusCode::BAD_REQUEST,
563 "AlreadyExistsException",
564 format!("Stack [{stack_name}] already exists"),
565 ));
566 }
567 }
568 }
569 store(&mut state.extras, "change_sets").insert(id.clone(), entry);
570 Ok(xml_response(
571 "CreateChangeSet",
572 format!(
573 " <Id>{}</Id>\n <StackId>{}</StackId>",
574 xml_escape(&id),
575 xml_escape(&stack_id_str)
576 ),
577 &rid,
578 ))
579 }
580 "DescribeChangeSet" => {
581 let cs = params
582 .get("ChangeSetName")
583 .ok_or_else(|| missing("ChangeSetName"))?
584 .clone();
585 let stack_filter = params.get("StackName").cloned();
586 let accounts = self.state.read();
587 let entry = accounts.get(&aid)
588 .and_then(|s| s.extras.get("change_sets"))
589 .and_then(|m| m.values().find(|v| {
590 let id_match = v["Id"].as_str() == Some(&cs)
591 || v["ChangeSetName"].as_str() == Some(&cs);
592 let stack_match = stack_filter.as_deref().is_none_or(|sf| {
593 v["StackName"].as_str() == Some(sf)
594 || v["StackId"].as_str() == Some(sf)
595 });
596 id_match && stack_match
597 }))
598 .cloned()
599 .unwrap_or_else(|| json!({"ChangeSetName": cs.clone(), "Status": "CREATE_COMPLETE", "ExecutionStatus": "AVAILABLE"}));
600 let changes_xml = entry["Changes"]
601 .as_array()
602 .map(|arr| {
603 let mut out = String::new();
604 for change in arr {
605 let rc = &change["ResourceChange"];
606 out.push_str(" <member>\n");
607 out.push_str(&format!(
608 " <Type>{}</Type>\n",
609 xml_escape(change["Type"].as_str().unwrap_or("Resource"))
610 ));
611 out.push_str(" <ResourceChange>\n");
612 out.push_str(&format!(
613 " <Action>{}</Action>\n",
614 xml_escape(rc["Action"].as_str().unwrap_or(""))
615 ));
616 out.push_str(&format!(
617 " <LogicalResourceId>{}</LogicalResourceId>\n",
618 xml_escape(rc["LogicalResourceId"].as_str().unwrap_or(""))
619 ));
620 if let Some(pid) = rc["PhysicalResourceId"].as_str() {
621 out.push_str(&format!(
622 " <PhysicalResourceId>{}</PhysicalResourceId>\n",
623 xml_escape(pid)
624 ));
625 }
626 out.push_str(&format!(
627 " <ResourceType>{}</ResourceType>\n",
628 xml_escape(rc["ResourceType"].as_str().unwrap_or(""))
629 ));
630 if let Some(replacement) = rc["Replacement"].as_str() {
631 out.push_str(&format!(
632 " <Replacement>{}</Replacement>\n",
633 xml_escape(replacement)
634 ));
635 }
636 out.push_str(" </ResourceChange>\n");
637 out.push_str(" </member>");
638 out.push('\n');
639 }
640 out
641 })
642 .unwrap_or_default();
643 let inner = format!(
644 " <ChangeSetName>{}</ChangeSetName>\n <ChangeSetId>{}</ChangeSetId>\n <StackId>{}</StackId>\n <StackName>{}</StackName>\n <Status>{}</Status>\n <ExecutionStatus>{}</ExecutionStatus>\n <Changes>\n{} </Changes>",
645 xml_escape(entry["ChangeSetName"].as_str().unwrap_or("")),
646 xml_escape(entry["Id"].as_str().unwrap_or("")),
647 xml_escape(entry["StackId"].as_str().unwrap_or("")),
648 xml_escape(entry["StackName"].as_str().unwrap_or("")),
649 xml_escape(entry["Status"].as_str().unwrap_or("CREATE_COMPLETE")),
650 xml_escape(entry["ExecutionStatus"].as_str().unwrap_or("AVAILABLE")),
651 changes_xml,
652 );
653 Ok(xml_response("DescribeChangeSet", inner, &rid))
654 }
655 "DescribeChangeSetHooks" => {
656 let cs = params
657 .get("ChangeSetName")
658 .ok_or_else(|| missing("ChangeSetName"))?
659 .clone();
660 let stack_filter = params.get("StackName").cloned();
661 let entry = {
665 let accounts = self.state.read();
666 accounts
667 .get(&aid)
668 .and_then(|s| s.extras.get("change_sets"))
669 .and_then(|m| {
670 m.values()
671 .find(|v| {
672 let id_match = v["Id"].as_str() == Some(&cs)
673 || v["ChangeSetName"].as_str() == Some(&cs);
674 let stack_match = stack_filter.as_deref().is_none_or(|sf| {
675 v["StackName"].as_str() == Some(sf)
676 || v["StackId"].as_str() == Some(sf)
677 });
678 id_match && stack_match
679 })
680 .cloned()
681 })
682 };
683 let (cs_id, cs_name, stack_id, stack_name, hooks) = match &entry {
684 Some(e) => (
685 e["Id"].as_str().unwrap_or("").to_string(),
686 e["ChangeSetName"].as_str().unwrap_or("").to_string(),
687 e["StackId"].as_str().unwrap_or("").to_string(),
688 e["StackName"].as_str().unwrap_or("").to_string(),
689 e["Hooks"].as_array().cloned().unwrap_or_default(),
690 ),
691 None => (
692 cs.clone(),
693 cs.clone(),
694 String::new(),
695 String::new(),
696 Vec::new(),
697 ),
698 };
699 let logical_filter = params.get("LogicalResourceId").cloned();
700 let hooks_xml = if hooks.is_empty() {
701 " <Hooks/>".to_string()
702 } else {
703 let members = members_xml(&hooks, |h| {
704 let type_name = h["TypeName"].as_str().unwrap_or("");
705 let resource_action =
706 logical_filter.as_deref().map(|_| "Modify").unwrap_or("Add");
707 let logical = logical_filter.as_deref().unwrap_or("");
708 format!(
709 " <InvocationPoint>PRE_PROVISION</InvocationPoint>\n <FailureMode>{}</FailureMode>\n <TypeName>{}</TypeName>\n <TypeVersionId>{}</TypeVersionId>\n <TypeConfigurationVersionId>{}</TypeConfigurationVersionId>\n <TargetDetails>\n <TargetType>RESOURCE</TargetType>\n <ResourceTargetDetails>\n <LogicalResourceId>{}</LogicalResourceId>\n <ResourceType>AWS::CloudFormation::Stack</ResourceType>\n <ResourceAction>{}</ResourceAction>\n </ResourceTargetDetails>\n </TargetDetails>",
710 xml_escape(h["FailureMode"].as_str().unwrap_or("FAIL")),
711 xml_escape(type_name),
712 xml_escape(h["TypeVersionId"].as_str().unwrap_or("00000001")),
713 xml_escape(h["TypeConfigurationVersionId"].as_str().unwrap_or("1")),
714 xml_escape(logical),
715 resource_action,
716 )
717 });
718 format!(" <Hooks>\n{members}\n </Hooks>")
719 };
720 let inner = format!(
721 " <ChangeSetId>{}</ChangeSetId>\n <ChangeSetName>{}</ChangeSetName>\n <StackId>{}</StackId>\n <StackName>{}</StackName>\n <Status>UNAVAILABLE</Status>\n{}",
722 xml_escape(&cs_id),
723 xml_escape(&cs_name),
724 xml_escape(&stack_id),
725 xml_escape(&stack_name),
726 hooks_xml,
727 );
728 Ok(xml_response("DescribeChangeSetHooks", inner, &rid))
729 }
730 "DeleteChangeSet" => {
731 let cs = params
732 .get("ChangeSetName")
733 .ok_or_else(|| missing("ChangeSetName"))?
734 .clone();
735 let mut accounts = self.state.write();
736 let state = accounts.get_or_create(&aid);
737 if let Some(m) = state.extras.get_mut("change_sets") {
738 m.retain(|_, v| {
739 v["Id"].as_str() != Some(&cs) && v["ChangeSetName"].as_str() != Some(&cs)
740 });
741 }
742 Ok(xml_response("DeleteChangeSet", String::new(), &rid))
743 }
744 "ExecuteChangeSet" => {
745 let cs = params
746 .get("ChangeSetName")
747 .cloned()
748 .ok_or_else(|| missing("ChangeSetName"))?;
749 let stack_filter = params.get("StackName").cloned();
750
751 let entry = {
752 let accounts = self.state.read();
753 accounts
754 .get(&aid)
755 .and_then(|s| s.extras.get("change_sets"))
756 .and_then(|m| {
757 m.values()
758 .find(|v| {
759 let id_match = v["Id"].as_str() == Some(&cs)
760 || v["ChangeSetName"].as_str() == Some(&cs);
761 let stack_match = stack_filter.as_deref().is_none_or(|sf| {
762 v["StackName"].as_str() == Some(sf)
763 || v["StackId"].as_str() == Some(sf)
764 });
765 id_match && stack_match
766 })
767 .cloned()
768 })
769 };
770 let Some(entry) = entry else {
771 return Ok(xml_response("ExecuteChangeSet", String::new(), &rid));
775 };
776
777 if entry["ExecutionStatus"].as_str() != Some("AVAILABLE") {
778 return Err(AwsServiceError::aws_error(
779 StatusCode::BAD_REQUEST,
780 "InvalidChangeSetStatus",
781 format!(
782 "ChangeSet [{cs}] cannot be executed in its current status of [{}]",
783 entry["ExecutionStatus"].as_str().unwrap_or("")
784 ),
785 ));
786 }
787
788 let cs_id = entry["Id"].as_str().unwrap_or("").to_string();
789 let stack_name = entry["StackName"].as_str().unwrap_or("").to_string();
790 let template_body = entry["TemplateBody"].as_str().unwrap_or("").to_string();
791 let cs_hooks: Vec<Value> = entry["Hooks"].as_array().cloned().unwrap_or_default();
792
793 let cs_tags: BTreeMap<String, String> = entry["Tags"]
794 .as_object()
795 .map(|m| {
796 m.iter()
797 .filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string())))
798 .collect()
799 })
800 .unwrap_or_default();
801 let cs_notif: Vec<String> = entry["NotificationArns"]
802 .as_array()
803 .map(|a| {
804 a.iter()
805 .filter_map(|v| v.as_str().map(|s| s.to_string()))
806 .collect()
807 })
808 .unwrap_or_default();
809 let mut cs_params: BTreeMap<String, String> = entry["Parameters"]
810 .as_object()
811 .map(|m| {
812 m.iter()
813 .filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string())))
814 .collect()
815 })
816 .unwrap_or_default();
817
818 let found: Option<String> = {
819 let accounts = self.state.read();
820 accounts.get(&aid).and_then(|s| {
821 s.stacks
822 .values()
823 .find(|st| {
824 (st.name == stack_name || st.stack_id == stack_name)
825 && st.status != "DELETE_COMPLETE"
826 })
827 .map(|st| st.stack_id.clone())
828 })
829 };
830
831 if template_body.trim().is_empty() {
838 let mut accounts = self.state.write();
839 let state = accounts.get_or_create(&aid);
840 if let Some(sid) = &found {
841 if let Some(stack) = state.stacks.values_mut().find(|s| &s.stack_id == sid)
842 {
843 if stack.status == "REVIEW_IN_PROGRESS" {
844 stack.status = "CREATE_COMPLETE".to_string();
845 stack.updated_at = Some(Utc::now());
846 }
847 }
848 }
849 if let Some(m) = state.extras.get_mut("change_sets") {
850 if let Some(e) = m.get_mut(&cs_id) {
851 e["ExecutionStatus"] = json!("EXECUTE_COMPLETE");
852 }
853 }
854 if !cs_hooks.is_empty() {
855 let target_id = found.clone().unwrap_or_else(|| cs_id.clone());
856 record_hook_results(
857 &mut state.extras,
858 &cs_hooks,
859 &HookTarget {
860 account_id: &aid,
861 target_type: "CLOUD_FORMATION",
862 target_id: &target_id,
863 logical_resource_id: &stack_name,
864 invocation_point: "PRE_PROVISION",
865 op_failed: false,
866 },
867 );
868 }
869 return Ok(xml_response("ExecuteChangeSet", String::new(), &rid));
870 }
871
872 let found_stack_id = found.ok_or_else(|| {
876 AwsServiceError::aws_error(
877 StatusCode::BAD_REQUEST,
878 "ValidationError",
879 format!("Stack [{stack_name}] does not exist"),
880 )
881 })?;
882
883 cs_params
884 .entry("AWS::Region".to_string())
885 .or_insert_with(|| req.region.clone());
886 cs_params
887 .entry("AWS::AccountId".to_string())
888 .or_insert_with(|| aid.clone());
889 cs_params
890 .entry("AWS::StackId".to_string())
891 .or_insert_with(|| found_stack_id.clone());
892 cs_params
893 .entry("AWS::StackName".to_string())
894 .or_insert_with(|| stack_name.clone());
895 cs_params
896 .entry("AWS::Partition".to_string())
897 .or_insert_with(|| "aws".to_string());
898 cs_params
899 .entry("AWS::URLSuffix".to_string())
900 .or_insert_with(|| "amazonaws.com".to_string());
901
902 let parsed = if template_body.trim().is_empty() {
906 template::ParsedTemplate {
907 description: None,
908 resources: Vec::new(),
909 outputs: Vec::new(),
910 }
911 } else {
912 template::parse_template(&template_body, &cs_params).map_err(|e| {
913 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationError", e)
914 })?
915 };
916
917 let provisioner = self.provisioner(&found_stack_id, &aid, &req.region);
918
919 let cs_imports =
922 CloudFormationService::collect_account_imports(&self.state, &aid, None);
923
924 let mut accounts = self.state.write();
925 let state = accounts.get_or_create(&aid);
926
927 let (update_result, sid, stack_name_owned, was_review, resources_snapshot) = {
934 let stack = state
935 .stacks
936 .values_mut()
937 .find(|st| st.stack_id == found_stack_id && st.status != "DELETE_COMPLETE")
938 .ok_or_else(|| {
939 AwsServiceError::aws_error(
940 StatusCode::BAD_REQUEST,
941 "ValidationError",
942 format!("Stack [{stack_name}] does not exist"),
943 )
944 })?;
945 let was_review = stack.status == "REVIEW_IN_PROGRESS";
946 stack.status = if was_review {
947 "CREATE_IN_PROGRESS"
948 } else {
949 "UPDATE_IN_PROGRESS"
950 }
951 .to_string();
952 let result = crate::service::apply_resource_updates(
953 stack,
954 &parsed.resources,
955 &template_body,
956 &cs_params,
957 &provisioner,
958 &cs_imports,
959 );
960 let sid = stack.stack_id.clone();
961 let sname = stack.name.clone();
962 stack.template = template_body.clone();
963 stack.status = match (was_review, result.is_err()) {
964 (true, false) => "CREATE_COMPLETE",
965 (true, true) => "ROLLBACK_COMPLETE",
966 (false, false) => "UPDATE_COMPLETE",
967 (false, true) => "UPDATE_ROLLBACK_COMPLETE",
968 }
969 .to_string();
970 stack.parameters = cs_params.clone();
971 if !cs_tags.is_empty() {
972 stack.tags = cs_tags;
973 }
974 if !cs_notif.is_empty() {
975 stack.notification_arns = cs_notif;
976 }
977 stack.updated_at = Some(Utc::now());
978 stack.outputs.clear();
981 let resources_snapshot = stack.resources.clone();
982 (result, sid, sname, was_review, resources_snapshot)
983 };
984
985 let (in_progress, complete, failed) = if was_review {
987 ("CREATE_IN_PROGRESS", "CREATE_COMPLETE", "ROLLBACK_COMPLETE")
988 } else {
989 (
990 "UPDATE_IN_PROGRESS",
991 "UPDATE_COMPLETE",
992 "UPDATE_ROLLBACK_COMPLETE",
993 )
994 };
995 crate::service::record_stack_status_event(
996 state,
997 &sid,
998 &stack_name_owned,
999 "AWS::CloudFormation::Stack",
1000 in_progress,
1001 );
1002 let final_status = match &update_result {
1003 Ok(changes) => {
1004 crate::service::record_stack_events(
1005 state,
1006 &sid,
1007 &stack_name_owned,
1008 changes,
1009 );
1010 complete
1011 }
1012 Err(_) => failed,
1013 };
1014 crate::service::record_stack_status_event(
1015 state,
1016 &sid,
1017 &stack_name_owned,
1018 "AWS::CloudFormation::Stack",
1019 final_status,
1020 );
1021
1022 if let Some(m) = state.extras.get_mut("change_sets") {
1023 if let Some(e) = m.get_mut(&cs_id) {
1024 e["ExecutionStatus"] = json!(if update_result.is_err() {
1025 "EXECUTE_FAILED"
1026 } else {
1027 "EXECUTE_COMPLETE"
1028 });
1029 }
1030 }
1031
1032 if !cs_hooks.is_empty() {
1037 record_hook_results(
1038 &mut state.extras,
1039 &cs_hooks,
1040 &HookTarget {
1041 account_id: &aid,
1042 target_type: "CLOUD_FORMATION",
1043 target_id: &sid,
1044 logical_resource_id: &stack_name_owned,
1045 invocation_point: "PRE_PROVISION",
1046 op_failed: update_result.is_err(),
1047 },
1048 );
1049 }
1050
1051 drop(accounts);
1052
1053 if let Err(msg) = update_result {
1054 return Err(AwsServiceError::aws_error(
1055 StatusCode::BAD_REQUEST,
1056 "ValidationError",
1057 msg,
1058 ));
1059 }
1060
1061 let outputs = CloudFormationService::resolve_template_outputs(
1069 &template_body,
1070 &cs_params,
1071 &resources_snapshot,
1072 &self.state,
1073 );
1074 {
1075 let mut accounts = self.state.write();
1076 let state = accounts.get_or_create(&aid);
1077 if let Some(stack) = state
1078 .stacks
1079 .values_mut()
1080 .find(|s| s.stack_id == sid && s.status != "DELETE_COMPLETE")
1081 {
1082 stack.outputs = outputs.clone();
1083 }
1084 CloudFormationService::sync_exports_imports(
1087 state,
1088 &sid,
1089 &stack_name_owned,
1090 &outputs,
1091 &[],
1092 );
1093 }
1094
1095 Ok(xml_response("ExecuteChangeSet", String::new(), &rid))
1096 }
1097 "ListChangeSets" => {
1098 require_scalar(¶ms, "StackName")?;
1099 let accounts = self.state.read();
1100 let items: Vec<Value> = accounts
1101 .get(&aid)
1102 .and_then(|s| s.extras.get("change_sets"))
1103 .map(|m| m.values().cloned().collect())
1104 .unwrap_or_default();
1105 let inner = format!(
1106 " <Summaries>\n{}\n </Summaries>",
1107 members_xml(&items, |v| {
1108 format!(
1109 " <ChangeSetId>{}</ChangeSetId>\n <ChangeSetName>{}</ChangeSetName>\n <Status>{}</Status>",
1110 xml_escape(v["Id"].as_str().unwrap_or("")),
1111 xml_escape(v["ChangeSetName"].as_str().unwrap_or("")),
1112 xml_escape(v["Status"].as_str().unwrap_or("CREATE_COMPLETE")),
1113 )
1114 }),
1115 );
1116 Ok(xml_response("ListChangeSets", inner, &rid))
1117 }
1118
1119 "CreateStackSet" => {
1121 let name = params
1122 .get("StackSetName")
1123 .ok_or_else(|| missing("StackSetName"))?
1124 .clone();
1125 let id = format!("{name}:{}", rand_id());
1126 let entry = json!({
1127 "StackSetId": id,
1128 "StackSetName": name,
1129 "Status": "ACTIVE",
1130 "TemplateBody": params.get("TemplateBody").cloned().unwrap_or_default(),
1131 });
1132 let mut accounts = self.state.write();
1133 let state = accounts.get_or_create(&aid);
1134 store(&mut state.extras, "stack_sets").insert(name.clone(), entry);
1135 Ok(xml_response(
1136 "CreateStackSet",
1137 format!(" <StackSetId>{}</StackSetId>", xml_escape(&id)),
1138 &rid,
1139 ))
1140 }
1141 "DescribeStackSet" => {
1142 let name = params
1143 .get("StackSetName")
1144 .ok_or_else(|| missing("StackSetName"))?
1145 .clone();
1146 let accounts = self.state.read();
1147 let entry = accounts
1148 .get(&aid)
1149 .and_then(|s| s.extras.get("stack_sets"))
1150 .and_then(|m| m.get(&name))
1151 .cloned()
1152 .unwrap_or_else(|| json!({"StackSetName": name.clone(), "Status": "ACTIVE"}));
1153 let inner = format!(
1154 " <StackSet>\n <StackSetName>{}</StackSetName>\n <StackSetId>{}</StackSetId>\n <Status>{}</Status>\n </StackSet>",
1155 xml_escape(entry["StackSetName"].as_str().unwrap_or(&name)),
1156 xml_escape(entry["StackSetId"].as_str().unwrap_or("")),
1157 xml_escape(entry["Status"].as_str().unwrap_or("ACTIVE")),
1158 );
1159 Ok(xml_response("DescribeStackSet", inner, &rid))
1160 }
1161 "ListStackSets" => {
1162 let accounts = self.state.read();
1163 let items: Vec<Value> = accounts
1164 .get(&aid)
1165 .and_then(|s| s.extras.get("stack_sets"))
1166 .map(|m| m.values().cloned().collect())
1167 .unwrap_or_default();
1168 let inner = format!(
1169 " <Summaries>\n{}\n </Summaries>",
1170 members_xml(&items, |v| {
1171 format!(
1172 " <StackSetName>{}</StackSetName>\n <StackSetId>{}</StackSetId>\n <Status>{}</Status>",
1173 xml_escape(v["StackSetName"].as_str().unwrap_or("")),
1174 xml_escape(v["StackSetId"].as_str().unwrap_or("")),
1175 xml_escape(v["Status"].as_str().unwrap_or("ACTIVE")),
1176 )
1177 }),
1178 );
1179 Ok(xml_response("ListStackSets", inner, &rid))
1180 }
1181 "UpdateStackSet" => {
1182 require_scalar(¶ms, "StackSetName")?;
1183 let op_id = rand_id();
1184 Ok(xml_response(
1185 "UpdateStackSet",
1186 format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
1187 &rid,
1188 ))
1189 }
1190 "DeleteStackSet" => {
1191 let name = params
1192 .get("StackSetName")
1193 .ok_or_else(|| missing("StackSetName"))?
1194 .clone();
1195 let mut accounts = self.state.write();
1196 let state = accounts.get_or_create(&aid);
1197 if let Some(m) = state.extras.get_mut("stack_sets") {
1198 m.remove(&name);
1199 }
1200 Ok(xml_response("DeleteStackSet", String::new(), &rid))
1201 }
1202 "DescribeStackSetOperation" => {
1203 require_scalar(¶ms, "StackSetName")?;
1204 require_scalar(¶ms, "OperationId")?;
1205 let op_id = params.get("OperationId").cloned().unwrap_or_else(rand_id);
1206 let inner = format!(
1207 " <StackSetOperation>\n <OperationId>{}</OperationId>\n <Status>SUCCEEDED</Status>\n </StackSetOperation>",
1208 xml_escape(&op_id),
1209 );
1210 Ok(xml_response("DescribeStackSetOperation", inner, &rid))
1211 }
1212 "ListStackSetOperations" => {
1213 require_scalar(¶ms, "StackSetName")?;
1214 Ok(xml_response(
1215 "ListStackSetOperations",
1216 " <Summaries/>".to_string(),
1217 &rid,
1218 ))
1219 }
1220 "ListStackSetOperationResults" => {
1221 require_scalar(¶ms, "StackSetName")?;
1222 require_scalar(¶ms, "OperationId")?;
1223 Ok(xml_response(
1224 "ListStackSetOperationResults",
1225 " <Summaries/>".to_string(),
1226 &rid,
1227 ))
1228 }
1229 "ListStackSetAutoDeploymentTargets" => {
1230 require_scalar(¶ms, "StackSetName")?;
1231 Ok(xml_response(
1232 "ListStackSetAutoDeploymentTargets",
1233 " <Summaries/>".to_string(),
1234 &rid,
1235 ))
1236 }
1237 "StopStackSetOperation" => {
1238 require_scalar(¶ms, "StackSetName")?;
1239 require_scalar(¶ms, "OperationId")?;
1240 Ok(xml_response("StopStackSetOperation", String::new(), &rid))
1241 }
1242 "ImportStacksToStackSet" => {
1243 require_scalar(¶ms, "StackSetName")?;
1244 let op_id = rand_id();
1245 Ok(xml_response(
1246 "ImportStacksToStackSet",
1247 format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
1248 &rid,
1249 ))
1250 }
1251
1252 "CreateStackInstances" => {
1260 require_scalar(¶ms, "StackSetName")?;
1261 let op_id = rand_id();
1262 Ok(xml_response(
1263 "CreateStackInstances",
1264 format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
1265 &rid,
1266 ))
1267 }
1268 "UpdateStackInstances" => {
1269 require_scalar(¶ms, "StackSetName")?;
1270 let op_id = rand_id();
1271 Ok(xml_response(
1272 "UpdateStackInstances",
1273 format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
1274 &rid,
1275 ))
1276 }
1277 "DeleteStackInstances" => {
1278 require_scalar(¶ms, "StackSetName")?;
1279 require_scalar(¶ms, "RetainStacks")?;
1280 let op_id = rand_id();
1281 Ok(xml_response(
1282 "DeleteStackInstances",
1283 format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
1284 &rid,
1285 ))
1286 }
1287 "DescribeStackInstance" => {
1288 require_scalar(¶ms, "StackSetName")?;
1289 require_scalar(¶ms, "StackInstanceAccount")?;
1290 require_scalar(¶ms, "StackInstanceRegion")?;
1291 let inner =
1292 " <StackInstance>\n <Status>CURRENT</Status>\n </StackInstance>"
1293 .to_string();
1294 Ok(xml_response("DescribeStackInstance", inner, &rid))
1295 }
1296 "ListStackInstances" => {
1297 require_scalar(¶ms, "StackSetName")?;
1298 Ok(xml_response(
1299 "ListStackInstances",
1300 " <Summaries/>".to_string(),
1301 &rid,
1302 ))
1303 }
1304 "ListStackInstanceResourceDrifts" => {
1305 require_scalar(¶ms, "StackSetName")?;
1306 require_scalar(¶ms, "StackInstanceAccount")?;
1307 require_scalar(¶ms, "StackInstanceRegion")?;
1308 require_scalar(¶ms, "OperationId")?;
1309 Ok(xml_response(
1310 "ListStackInstanceResourceDrifts",
1311 " <Summaries/>".to_string(),
1312 &rid,
1313 ))
1314 }
1315
1316 "CreateStackRefactor" => {
1318 require_collection(¶ms, "StackDefinitions")?;
1319 let id = rand_id();
1320 let entry = json!({"StackRefactorId": id.clone(), "Status": "CREATE_COMPLETE"});
1321 let mut accounts = self.state.write();
1322 let state = accounts.get_or_create(&aid);
1323 store(&mut state.extras, "refactors").insert(id.clone(), entry);
1324 Ok(xml_response(
1325 "CreateStackRefactor",
1326 format!(" <StackRefactorId>{}</StackRefactorId>", xml_escape(&id)),
1327 &rid,
1328 ))
1329 }
1330 "DescribeStackRefactor" => {
1331 let id = params
1332 .get("StackRefactorId")
1333 .ok_or_else(|| missing("StackRefactorId"))?
1334 .clone();
1335 let inner = format!(
1336 " <StackRefactorId>{}</StackRefactorId>\n <Status>CREATE_COMPLETE</Status>",
1337 xml_escape(&id),
1338 );
1339 Ok(xml_response("DescribeStackRefactor", inner, &rid))
1340 }
1341 "ExecuteStackRefactor" => {
1342 require_scalar(¶ms, "StackRefactorId")?;
1343 Ok(xml_response("ExecuteStackRefactor", String::new(), &rid))
1344 }
1345 "ListStackRefactors" => Ok(xml_response(
1346 "ListStackRefactors",
1347 " <StackRefactorSummaries/>".to_string(),
1348 &rid,
1349 )),
1350 "ListStackRefactorActions" => {
1351 require_scalar(¶ms, "StackRefactorId")?;
1352 Ok(xml_response(
1353 "ListStackRefactorActions",
1354 " <StackRefactorActions/>".to_string(),
1355 &rid,
1356 ))
1357 }
1358
1359 "ActivateType" => {
1361 let arn = Arn::new(
1362 "cloudformation",
1363 "us-east-1",
1364 &aid,
1365 &format!("type/resource/{}", rand_id()),
1366 )
1367 .to_string();
1368 let type_name = params
1373 .get("TypeNameAlias")
1374 .or_else(|| params.get("TypeName"));
1375 if is_hook_type(
1376 params.get("Type").map(String::as_str),
1377 type_name.map(String::as_str),
1378 ) {
1379 if let Some(name) = type_name {
1380 let mut accounts = self.state.write();
1381 let state = accounts.get_or_create(&aid);
1382 register_hook(&mut state.extras, name, None, None);
1383 }
1384 }
1385 Ok(xml_response(
1386 "ActivateType",
1387 format!(" <Arn>{}</Arn>", xml_escape(&arn)),
1388 &rid,
1389 ))
1390 }
1391 "DeactivateType" => Ok(xml_response("DeactivateType", String::new(), &rid)),
1392 "DescribeType" => {
1393 let arn = params.get("Arn").cloned().unwrap_or_else(|| {
1394 Arn::new("cloudformation", "us-east-1", &aid, "type/resource/Default")
1395 .to_string()
1396 });
1397 let inner = format!(
1398 " <Arn>{}</Arn>\n <Type>RESOURCE</Type>\n <TypeName>AWS::Custom::Type</TypeName>",
1399 xml_escape(&arn),
1400 );
1401 Ok(xml_response("DescribeType", inner, &rid))
1402 }
1403 "DescribeTypeRegistration" => {
1404 let token = params
1405 .get("RegistrationToken")
1406 .cloned()
1407 .ok_or_else(|| missing("RegistrationToken"))?;
1408 let inner = format!(
1409 " <ProgressStatus>COMPLETE</ProgressStatus>\n <Description>{}</Description>",
1410 xml_escape(&token),
1411 );
1412 Ok(xml_response("DescribeTypeRegistration", inner, &rid))
1413 }
1414 "RegisterType" => {
1415 require_scalar(¶ms, "TypeName")?;
1416 require_scalar(¶ms, "SchemaHandlerPackage")?;
1417 if is_hook_type(
1418 params.get("Type").map(String::as_str),
1419 params.get("TypeName").map(String::as_str),
1420 ) {
1421 if let Some(name) = params.get("TypeName") {
1422 let mut accounts = self.state.write();
1423 let state = accounts.get_or_create(&aid);
1424 register_hook(&mut state.extras, name, None, None);
1425 }
1426 }
1427 let token = rand_id();
1428 Ok(xml_response(
1429 "RegisterType",
1430 format!(
1431 " <RegistrationToken>{}</RegistrationToken>",
1432 xml_escape(&token)
1433 ),
1434 &rid,
1435 ))
1436 }
1437 "DeregisterType" => Ok(xml_response("DeregisterType", String::new(), &rid)),
1438 "ListTypes" => Ok(xml_response(
1439 "ListTypes",
1440 " <TypeSummaries/>".to_string(),
1441 &rid,
1442 )),
1443 "ListTypeRegistrations" => Ok(xml_response(
1444 "ListTypeRegistrations",
1445 " <RegistrationTokenList/>".to_string(),
1446 &rid,
1447 )),
1448 "ListTypeVersions" => Ok(xml_response(
1449 "ListTypeVersions",
1450 " <TypeVersionSummaries/>".to_string(),
1451 &rid,
1452 )),
1453 "BatchDescribeTypeConfigurations" => {
1454 Ok(xml_response(
1458 "BatchDescribeTypeConfigurations",
1459 " <Errors/>\n <TypeConfigurations/>".to_string(),
1460 &rid,
1461 ))
1462 }
1463 "SetTypeConfiguration" => {
1464 require_scalar(¶ms, "Configuration")?;
1465 let configuration = params.get("Configuration");
1471 if is_hook_type(
1472 params.get("Type").map(String::as_str),
1473 params.get("TypeName").map(String::as_str),
1474 ) {
1475 if let Some(name) = params.get("TypeName") {
1476 let failure_mode = configuration
1477 .and_then(|c| serde_json::from_str::<Value>(c).ok())
1478 .as_ref()
1479 .and_then(|c| {
1480 c.get("CloudFormationConfiguration")
1481 .and_then(|h| h.get("HookConfiguration"))
1482 .and_then(|h| h.get("FailureMode"))
1483 .and_then(Value::as_str)
1484 })
1485 .map(str::to_string);
1486 let mut accounts = self.state.write();
1487 let state = accounts.get_or_create(&aid);
1488 register_hook(
1489 &mut state.extras,
1490 name,
1491 failure_mode.as_deref(),
1492 configuration.map(String::as_str),
1493 );
1494 }
1495 }
1496 let arn = Arn::new(
1497 "cloudformation",
1498 "us-east-1",
1499 &aid,
1500 &format!("type-config/{}", rand_id()),
1501 )
1502 .to_string();
1503 Ok(xml_response(
1504 "SetTypeConfiguration",
1505 format!(
1506 " <ConfigurationArn>{}</ConfigurationArn>",
1507 xml_escape(&arn)
1508 ),
1509 &rid,
1510 ))
1511 }
1512 "SetTypeDefaultVersion" => {
1513 Ok(xml_response("SetTypeDefaultVersion", String::new(), &rid))
1514 }
1515 "TestType" => {
1516 let arn = Arn::new(
1517 "cloudformation",
1518 "us-east-1",
1519 &aid,
1520 &format!("type/resource/{}", rand_id()),
1521 )
1522 .to_string();
1523 Ok(xml_response(
1524 "TestType",
1525 format!(" <TypeVersionArn>{}</TypeVersionArn>", xml_escape(&arn)),
1526 &rid,
1527 ))
1528 }
1529 "PublishType" => {
1530 let arn = Arn::new(
1531 "cloudformation",
1532 "us-east-1",
1533 &aid,
1534 &format!("type/resource/{}", rand_id()),
1535 )
1536 .to_string();
1537 Ok(xml_response(
1538 "PublishType",
1539 format!(" <PublicTypeArn>{}</PublicTypeArn>", xml_escape(&arn)),
1540 &rid,
1541 ))
1542 }
1543 "RegisterPublisher" => {
1544 let id = rand_id();
1545 Ok(xml_response(
1546 "RegisterPublisher",
1547 format!(" <PublisherId>{}</PublisherId>", xml_escape(&id)),
1548 &rid,
1549 ))
1550 }
1551 "DescribePublisher" => {
1552 let id = params
1553 .get("PublisherId")
1554 .cloned()
1555 .unwrap_or_else(|| "default-publisher".to_string());
1556 let inner = format!(
1557 " <PublisherId>{}</PublisherId>\n <PublisherStatus>VERIFIED</PublisherStatus>\n <IdentityProvider>AWS_Marketplace</IdentityProvider>",
1558 xml_escape(&id),
1559 );
1560 Ok(xml_response("DescribePublisher", inner, &rid))
1561 }
1562
1563 "CreateGeneratedTemplate" => {
1565 let name = params
1566 .get("GeneratedTemplateName")
1567 .ok_or_else(|| missing("GeneratedTemplateName"))?
1568 .clone();
1569 let id = Arn::new(
1570 "cloudformation",
1571 "us-east-1",
1572 &aid,
1573 &format!("generatedtemplate/{}", rand_id()),
1574 )
1575 .to_string();
1576 let entry = json!({"GeneratedTemplateId": id.clone(), "Name": name.clone(), "Status": "COMPLETE"});
1577 let mut accounts = self.state.write();
1578 let state = accounts.get_or_create(&aid);
1579 store(&mut state.extras, "generated_templates").insert(name.clone(), entry);
1580 Ok(xml_response(
1581 "CreateGeneratedTemplate",
1582 format!(
1583 " <GeneratedTemplateId>{}</GeneratedTemplateId>",
1584 xml_escape(&id)
1585 ),
1586 &rid,
1587 ))
1588 }
1589 "UpdateGeneratedTemplate" => {
1590 let name = params
1591 .get("GeneratedTemplateName")
1592 .ok_or_else(|| missing("GeneratedTemplateName"))?
1593 .clone();
1594 let id = Arn::new(
1595 "cloudformation",
1596 "us-east-1",
1597 &aid,
1598 &format!("generatedtemplate/{name}"),
1599 )
1600 .to_string();
1601 Ok(xml_response(
1602 "UpdateGeneratedTemplate",
1603 format!(
1604 " <GeneratedTemplateId>{}</GeneratedTemplateId>",
1605 xml_escape(&id)
1606 ),
1607 &rid,
1608 ))
1609 }
1610 "DescribeGeneratedTemplate" => {
1611 let name = params
1612 .get("GeneratedTemplateName")
1613 .ok_or_else(|| missing("GeneratedTemplateName"))?
1614 .clone();
1615 let inner = format!(
1616 " <GeneratedTemplateId>arn:aws:cloudformation:us-east-1:{}:generatedtemplate/{}</GeneratedTemplateId>\n <GeneratedTemplateName>{}</GeneratedTemplateName>\n <Status>COMPLETE</Status>",
1617 xml_escape(&aid),
1618 xml_escape(&name),
1619 xml_escape(&name),
1620 );
1621 Ok(xml_response("DescribeGeneratedTemplate", inner, &rid))
1622 }
1623 "GetGeneratedTemplate" => {
1624 require_scalar(¶ms, "GeneratedTemplateName")?;
1625 Ok(xml_response(
1626 "GetGeneratedTemplate",
1627 " <Status>COMPLETE</Status>\n <TemplateBody>{}</TemplateBody>"
1628 .to_string(),
1629 &rid,
1630 ))
1631 }
1632 "DeleteGeneratedTemplate" => {
1633 let name = params
1634 .get("GeneratedTemplateName")
1635 .ok_or_else(|| missing("GeneratedTemplateName"))?
1636 .clone();
1637 let mut accounts = self.state.write();
1638 let state = accounts.get_or_create(&aid);
1639 if let Some(m) = state.extras.get_mut("generated_templates") {
1640 m.remove(&name);
1641 }
1642 Ok(xml_response("DeleteGeneratedTemplate", String::new(), &rid))
1643 }
1644 "ListGeneratedTemplates" => Ok(xml_response(
1645 "ListGeneratedTemplates",
1646 " <Summaries/>".to_string(),
1647 &rid,
1648 )),
1649
1650 "StartResourceScan" => {
1652 let id = Arn::new(
1653 "cloudformation",
1654 "us-east-1",
1655 &aid,
1656 &format!("resourceScan/{}", rand_id()),
1657 )
1658 .to_string();
1659 Ok(xml_response(
1660 "StartResourceScan",
1661 format!(" <ResourceScanId>{}</ResourceScanId>", xml_escape(&id)),
1662 &rid,
1663 ))
1664 }
1665 "DescribeResourceScan" => {
1666 let id = params
1667 .get("ResourceScanId")
1668 .cloned()
1669 .ok_or_else(|| missing("ResourceScanId"))?;
1670 let inner = format!(
1671 " <ResourceScanId>{}</ResourceScanId>\n <Status>COMPLETE</Status>",
1672 xml_escape(&id),
1673 );
1674 Ok(xml_response("DescribeResourceScan", inner, &rid))
1675 }
1676 "ListResourceScans" => Ok(xml_response(
1677 "ListResourceScans",
1678 " <ResourceScanSummaries/>".to_string(),
1679 &rid,
1680 )),
1681 "ListResourceScanResources" => {
1682 require_scalar(¶ms, "ResourceScanId")?;
1683 Ok(xml_response(
1684 "ListResourceScanResources",
1685 " <Resources/>".to_string(),
1686 &rid,
1687 ))
1688 }
1689 "ListResourceScanRelatedResources" => {
1690 require_scalar(¶ms, "ResourceScanId")?;
1691 Ok(xml_response(
1692 "ListResourceScanRelatedResources",
1693 " <RelatedResources/>".to_string(),
1694 &rid,
1695 ))
1696 }
1697
1698 "DetectStackDrift" => {
1700 let stack_name = params
1701 .get("StackName")
1702 .ok_or_else(|| missing("StackName"))?
1703 .clone();
1704 let id = rand_id();
1705
1706 let resources: Vec<StackResource> = {
1707 let accounts = self.state.read();
1708 let stack = accounts.get(&aid).and_then(|s| {
1709 s.stacks.values().find(|st| {
1710 (st.name == stack_name || st.stack_id == stack_name)
1711 && st.status != "DELETE_COMPLETE"
1712 })
1713 });
1714 stack.map(|s| s.resources.clone()).unwrap_or_default()
1715 };
1716
1717 let mut drifted_resources: Vec<Value> = Vec::new();
1718
1719 for resource in &resources {
1720 let exists = match resource.resource_type.as_str() {
1721 "AWS::SQS::Queue" => self
1722 .deps
1723 .sqs
1724 .read()
1725 .get(&aid)
1726 .map(|s| s.queues.contains_key(&resource.physical_id))
1727 .unwrap_or(false),
1728 "AWS::SNS::Topic" => self
1729 .deps
1730 .sns
1731 .read()
1732 .get(&aid)
1733 .map(|s| s.topics.contains_key(&resource.physical_id))
1734 .unwrap_or(false),
1735 "AWS::S3::Bucket" => self
1736 .deps
1737 .s3
1738 .read()
1739 .get(&aid)
1740 .map(|s| s.buckets.contains_key(&resource.physical_id))
1741 .unwrap_or(false),
1742 "AWS::Lambda::Function" => self
1743 .deps
1744 .lambda
1745 .read()
1746 .get(&aid)
1747 .map(|s| s.functions.contains_key(&resource.physical_id))
1748 .unwrap_or(false),
1749 "AWS::IAM::Role" => self
1750 .deps
1751 .iam
1752 .read()
1753 .get(&aid)
1754 .map(|s| s.roles.contains_key(&resource.physical_id))
1755 .unwrap_or(false),
1756 "AWS::DynamoDB::Table" => self
1757 .deps
1758 .dynamodb
1759 .read()
1760 .get(&aid)
1761 .map(|s| s.tables.values().any(|t| t.arn == resource.physical_id))
1762 .unwrap_or(false),
1763 "AWS::KMS::Key" => self
1764 .deps
1765 .kms
1766 .read()
1767 .get(&aid)
1768 .map(|s| s.keys.contains_key(&resource.physical_id))
1769 .unwrap_or(false),
1770 "AWS::SecretsManager::Secret" => self
1771 .deps
1772 .secretsmanager
1773 .read()
1774 .get(&aid)
1775 .map(|s| s.secrets.contains_key(&resource.physical_id))
1776 .unwrap_or(false),
1777 _ => true, };
1779 if !exists {
1780 drifted_resources.push(json!({
1781 "LogicalResourceId": resource.logical_id,
1782 "PhysicalResourceId": resource.physical_id,
1783 "ResourceType": resource.resource_type,
1784 "StackResourceDriftStatus": "DELETED",
1785 "PropertyDifferences": [],
1786 }));
1787 }
1788 }
1789
1790 let stack_drift_status = if drifted_resources.is_empty() {
1791 "IN_SYNC"
1792 } else {
1793 "DRIFTED"
1794 };
1795
1796 let record = json!({
1797 "StackDriftDetectionId": id,
1798 "StackName": stack_name,
1799 "StackDriftStatus": stack_drift_status,
1800 "DetectionStatus": "DETECTION_COMPLETE",
1801 "DriftedResources": drifted_resources,
1802 });
1803
1804 {
1805 let mut accounts = self.state.write();
1806 let state = accounts.get_or_create(&aid);
1807 store(&mut state.extras, "drift_detection").insert(id.clone(), record);
1808 }
1809
1810 Ok(xml_response(
1811 "DetectStackDrift",
1812 format!(
1813 " <StackDriftDetectionId>{}</StackDriftDetectionId>",
1814 xml_escape(&id)
1815 ),
1816 &rid,
1817 ))
1818 }
1819 "DetectStackResourceDrift" => {
1820 let stack_name = params
1821 .get("StackName")
1822 .ok_or_else(|| missing("StackName"))?
1823 .clone();
1824 let logical = params
1825 .get("LogicalResourceId")
1826 .ok_or_else(|| missing("LogicalResourceId"))?
1827 .clone();
1828 let accounts = self.state.read();
1829 let resource_drift = accounts
1830 .get(&aid)
1831 .and_then(|s| {
1832 s.stacks.values().find(|st| {
1833 (st.name == stack_name || st.stack_id == stack_name)
1834 && st.status != "DELETE_COMPLETE"
1835 })
1836 })
1837 .and_then(|stack| stack.resources.iter().find(|r| r.logical_id == logical))
1838 .map(|resource| {
1839 let exists = match resource.resource_type.as_str() {
1840 "AWS::SQS::Queue" => self
1841 .deps
1842 .sqs
1843 .read()
1844 .get(&aid)
1845 .map(|s| s.queues.contains_key(&resource.physical_id))
1846 .unwrap_or(false),
1847 "AWS::SNS::Topic" => self
1848 .deps
1849 .sns
1850 .read()
1851 .get(&aid)
1852 .map(|s| s.topics.contains_key(&resource.physical_id))
1853 .unwrap_or(false),
1854 "AWS::S3::Bucket" => self
1855 .deps
1856 .s3
1857 .read()
1858 .get(&aid)
1859 .map(|s| s.buckets.contains_key(&resource.physical_id))
1860 .unwrap_or(false),
1861 "AWS::Lambda::Function" => self
1862 .deps
1863 .lambda
1864 .read()
1865 .get(&aid)
1866 .map(|s| s.functions.contains_key(&resource.physical_id))
1867 .unwrap_or(false),
1868 "AWS::IAM::Role" => self
1869 .deps
1870 .iam
1871 .read()
1872 .get(&aid)
1873 .map(|s| s.roles.contains_key(&resource.physical_id))
1874 .unwrap_or(false),
1875 "AWS::DynamoDB::Table" => self
1876 .deps
1877 .dynamodb
1878 .read()
1879 .get(&aid)
1880 .map(|s| s.tables.values().any(|t| t.arn == resource.physical_id))
1881 .unwrap_or(false),
1882 "AWS::KMS::Key" => self
1883 .deps
1884 .kms
1885 .read()
1886 .get(&aid)
1887 .map(|s| s.keys.contains_key(&resource.physical_id))
1888 .unwrap_or(false),
1889 "AWS::SecretsManager::Secret" => self
1890 .deps
1891 .secretsmanager
1892 .read()
1893 .get(&aid)
1894 .map(|s| s.secrets.contains_key(&resource.physical_id))
1895 .unwrap_or(false),
1896 _ => true,
1897 };
1898 if exists {
1899 "IN_SYNC"
1900 } else {
1901 "DELETED"
1902 }
1903 })
1904 .unwrap_or("NOT_CHECKED");
1905
1906 let inner = format!(
1907 " <StackResourceDrift>\n <LogicalResourceId>{}</LogicalResourceId>\n <StackResourceDriftStatus>{}</StackResourceDriftStatus>\n </StackResourceDrift>",
1908 xml_escape(&logical),
1909 xml_escape(resource_drift),
1910 );
1911 Ok(xml_response("DetectStackResourceDrift", inner, &rid))
1912 }
1913 "DetectStackSetDrift" => {
1914 require_scalar(¶ms, "StackSetName")?;
1915 let op_id = rand_id();
1916 Ok(xml_response(
1917 "DetectStackSetDrift",
1918 format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
1919 &rid,
1920 ))
1921 }
1922 "DescribeStackDriftDetectionStatus" => {
1923 let id = params
1924 .get("StackDriftDetectionId")
1925 .ok_or_else(|| missing("StackDriftDetectionId"))?
1926 .clone();
1927 let accounts = self.state.read();
1928 let record = accounts
1929 .get(&aid)
1930 .and_then(|s| s.extras.get("drift_detection"))
1931 .and_then(|m| m.get(&id))
1932 .cloned()
1933 .unwrap_or_else(|| {
1934 json!({
1935 "StackDriftDetectionId": id,
1936 "StackDriftStatus": "IN_SYNC",
1937 "DetectionStatus": "DETECTION_COMPLETE",
1938 })
1939 });
1940 let stack_id = record["StackId"]
1946 .as_str()
1947 .map(str::to_owned)
1948 .unwrap_or_else(|| {
1949 Arn::new(
1950 "cloudformation",
1951 "us-east-1",
1952 &aid,
1953 &format!("stack/drift-{id}/{}", rand_id()),
1954 )
1955 .to_string()
1956 });
1957 let timestamp = record["Timestamp"]
1958 .as_str()
1959 .map(str::to_owned)
1960 .unwrap_or_else(|| "2024-01-01T00:00:00Z".to_string());
1961
1962 let inner = format!(
1963 " <StackId>{}</StackId>\n <StackDriftDetectionId>{}</StackDriftDetectionId>\n <DetectionStatus>{}</DetectionStatus>\n <StackDriftStatus>{}</StackDriftStatus>\n <Timestamp>{}</Timestamp>",
1964 xml_escape(&stack_id),
1965 xml_escape(record["StackDriftDetectionId"].as_str().unwrap_or("")),
1966 xml_escape(record["DetectionStatus"].as_str().unwrap_or("DETECTION_COMPLETE")),
1967 xml_escape(record["StackDriftStatus"].as_str().unwrap_or("IN_SYNC")),
1968 xml_escape(×tamp),
1969 );
1970 Ok(xml_response(
1971 "DescribeStackDriftDetectionStatus",
1972 inner,
1973 &rid,
1974 ))
1975 }
1976 "DescribeStackResourceDrifts" => {
1977 let stack_name = params
1978 .get("StackName")
1979 .cloned()
1980 .ok_or_else(|| missing("StackName"))?;
1981 let accounts = self.state.read();
1982 let drifted: Vec<Value> = accounts
1983 .get(&aid)
1984 .and_then(|s| {
1985 let found = s
1986 .stacks
1987 .values()
1988 .find(|st| {
1989 (st.name == stack_name || st.stack_id == stack_name)
1990 && st.status != "DELETE_COMPLETE"
1991 })
1992 .is_some();
1993 if !found {
1994 return None;
1995 }
1996 s.extras
1997 .get("drift_detection")
1998 .and_then(|m| {
1999 m.values()
2000 .find(|v| v["StackName"].as_str() == Some(stack_name.as_str()))
2001 })
2002 .and_then(|v| v["DriftedResources"].as_array().cloned())
2003 })
2004 .unwrap_or_default();
2005
2006 let inner = if drifted.is_empty() {
2007 " <StackResourceDrifts/>".to_string()
2008 } else {
2009 format!(
2010 " <StackResourceDrifts>\n{}\n </StackResourceDrifts>",
2011 members_xml(&drifted, |v| {
2012 format!(
2013 " <StackResourceDrift>\n <LogicalResourceId>{}</LogicalResourceId>\n <PhysicalResourceId>{}</PhysicalResourceId>\n <ResourceType>{}</ResourceType>\n <StackResourceDriftStatus>{}</StackResourceDriftStatus>\n </StackResourceDrift>",
2014 xml_escape(v["LogicalResourceId"].as_str().unwrap_or("")),
2015 xml_escape(v["PhysicalResourceId"].as_str().unwrap_or("")),
2016 xml_escape(v["ResourceType"].as_str().unwrap_or("")),
2017 xml_escape(v["StackResourceDriftStatus"].as_str().unwrap_or("IN_SYNC")),
2018 )
2019 }),
2020 )
2021 };
2022 Ok(xml_response("DescribeStackResourceDrifts", inner, &rid))
2023 }
2024 "DescribeStackResource" => {
2025 let stack_name = params
2026 .get("StackName")
2027 .ok_or_else(|| missing("StackName"))?
2028 .clone();
2029 let logical = params
2030 .get("LogicalResourceId")
2031 .ok_or_else(|| missing("LogicalResourceId"))?
2032 .clone();
2033 let accounts = self.state.read();
2034 let detail = accounts
2035 .get(&aid)
2036 .and_then(|s| s.stacks.get(&stack_name))
2037 .and_then(|s| s.resources.iter().find(|r| r.logical_id == logical))
2038 .map(|r| {
2039 (
2040 r.physical_id.clone(),
2041 r.resource_type.clone(),
2042 r.status.clone(),
2043 )
2044 })
2045 .unwrap_or_else(|| {
2046 (
2047 "pid".to_string(),
2048 "AWS::Custom".to_string(),
2049 "CREATE_COMPLETE".to_string(),
2050 )
2051 });
2052 let inner = format!(
2053 " <StackResourceDetail>\n <StackName>{}</StackName>\n <LogicalResourceId>{}</LogicalResourceId>\n <PhysicalResourceId>{}</PhysicalResourceId>\n <ResourceType>{}</ResourceType>\n <ResourceStatus>{}</ResourceStatus>\n <LastUpdatedTimestamp>{}</LastUpdatedTimestamp>\n </StackResourceDetail>",
2054 xml_escape(&stack_name),
2055 xml_escape(&logical),
2056 xml_escape(&detail.0),
2057 xml_escape(&detail.1),
2058 xml_escape(&detail.2),
2059 chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ"),
2060 );
2061 Ok(xml_response("DescribeStackResource", inner, &rid))
2062 }
2063
2064 "DescribeStackEvents" => {
2066 require_scalar(¶ms, "StackName")?;
2067 let stack_filter = params.get("StackName").cloned();
2068 let accounts = self.state.read();
2069 let events: Vec<Value> = accounts
2070 .get(&aid)
2071 .map(|s| {
2072 let mut all: Vec<Value> = Vec::new();
2073 for (sid, evs) in &s.events {
2074 let matches = match &stack_filter {
2076 None => true,
2077 Some(filter) => {
2078 sid == filter
2079 || s.stacks.values().any(|st| {
2080 st.stack_id == *sid
2081 && (st.name == *filter || st.stack_id == *filter)
2082 })
2083 }
2084 };
2085 if matches {
2086 all.extend(evs.iter().cloned());
2087 }
2088 }
2089 all.reverse();
2091 all
2092 })
2093 .unwrap_or_default();
2094 let inner = if events.is_empty() {
2095 " <StackEvents/>".to_string()
2096 } else {
2097 format!(
2098 " <StackEvents>\n{}\n </StackEvents>",
2099 members_xml(&events, |v| {
2100 format!(
2101 " <EventId>{}</EventId>\n <StackId>{}</StackId>\n <StackName>{}</StackName>\n <LogicalResourceId>{}</LogicalResourceId>\n <PhysicalResourceId>{}</PhysicalResourceId>\n <ResourceType>{}</ResourceType>\n <ResourceStatus>{}</ResourceStatus>\n <Timestamp>{}</Timestamp>",
2102 xml_escape(v["EventId"].as_str().unwrap_or("")),
2103 xml_escape(v["StackId"].as_str().unwrap_or("")),
2104 xml_escape(v["StackName"].as_str().unwrap_or("")),
2105 xml_escape(v["LogicalResourceId"].as_str().unwrap_or("")),
2106 xml_escape(v["PhysicalResourceId"].as_str().unwrap_or("")),
2107 xml_escape(v["ResourceType"].as_str().unwrap_or("")),
2108 xml_escape(v["ResourceStatus"].as_str().unwrap_or("")),
2109 xml_escape(v["Timestamp"].as_str().unwrap_or("")),
2110 )
2111 }),
2112 )
2113 };
2114 Ok(xml_response("DescribeStackEvents", inner, &rid))
2115 }
2116 "DescribeEvents" => Ok(xml_response(
2117 "DescribeEvents",
2118 " <Events/>".to_string(),
2119 &rid,
2120 )),
2121
2122 "GetHookResult" => {
2124 let result_id = params
2132 .get("HookResultId")
2133 .or_else(|| params.get("HookId"))
2134 .cloned()
2135 .unwrap_or_default();
2136 let record = {
2137 let accounts = self.state.read();
2138 accounts
2139 .get(&aid)
2140 .and_then(|s| s.extras.get("hook_results"))
2141 .and_then(|m| m.get(&result_id))
2142 .cloned()
2143 };
2144 let r = record.unwrap_or_else(|| {
2145 serde_json::json!({
2146 "HookResultId": result_id,
2147 "InvocationPoint": params
2148 .get("InvocationPoint")
2149 .cloned()
2150 .unwrap_or_default(),
2151 "Status": "",
2152 "HookStatusReason": "",
2153 })
2154 });
2155 let inner = format!(
2156 " <HookResultId>{}</HookResultId>\n <InvocationPoint>{}</InvocationPoint>\n <FailureMode>{}</FailureMode>\n <TypeName>{}</TypeName>\n <TypeVersionId>{}</TypeVersionId>\n <TypeConfigurationVersionId>{}</TypeConfigurationVersionId>\n <TypeArn>{}</TypeArn>\n <Status>{}</Status>\n <HookStatusReason>{}</HookStatusReason>",
2157 xml_escape(r["HookResultId"].as_str().unwrap_or("")),
2158 xml_escape(r["InvocationPoint"].as_str().unwrap_or("PRE_PROVISION")),
2159 xml_escape(r["FailureMode"].as_str().unwrap_or("FAIL")),
2160 xml_escape(r["TypeName"].as_str().unwrap_or("")),
2161 xml_escape(r["TypeVersionId"].as_str().unwrap_or("00000001")),
2162 xml_escape(r["TypeConfigurationVersionId"].as_str().unwrap_or("1")),
2163 xml_escape(r["TypeArn"].as_str().unwrap_or("")),
2164 xml_escape(r["Status"].as_str().unwrap_or("HOOK_COMPLETE_SUCCEEDED")),
2165 xml_escape(r["HookStatusReason"].as_str().unwrap_or("")),
2166 );
2167 Ok(xml_response("GetHookResult", inner, &rid))
2168 }
2169 "ListHookResults" => {
2170 let target_type = params.get("TargetType").cloned();
2174 let target_id = params.get("TargetId").cloned();
2175 let type_arn = params.get("TypeArn").cloned();
2176 let status_filter = params.get("Status").cloned();
2177 let records: Vec<Value> = {
2178 let accounts = self.state.read();
2179 accounts
2180 .get(&aid)
2181 .and_then(|s| s.extras.get("hook_results"))
2182 .map(|m| {
2183 m.values()
2184 .filter(|r| {
2185 target_type
2186 .as_deref()
2187 .is_none_or(|t| r["TargetType"].as_str() == Some(t))
2188 && target_id
2189 .as_deref()
2190 .is_none_or(|t| r["TargetId"].as_str() == Some(t))
2191 && type_arn
2192 .as_deref()
2193 .is_none_or(|t| r["TypeArn"].as_str() == Some(t))
2194 && status_filter
2195 .as_deref()
2196 .is_none_or(|s| r["Status"].as_str() == Some(s))
2197 })
2198 .cloned()
2199 .collect()
2200 })
2201 .unwrap_or_default()
2202 };
2203 let results_xml = if records.is_empty() {
2204 " <HookResults/>".to_string()
2205 } else {
2206 let members = members_xml(&records, |r| {
2207 format!(
2208 " <HookResultId>{}</HookResultId>\n <InvocationPoint>{}</InvocationPoint>\n <FailureMode>{}</FailureMode>\n <TypeName>{}</TypeName>\n <TypeVersionId>{}</TypeVersionId>\n <TypeConfigurationVersionId>{}</TypeConfigurationVersionId>\n <TypeArn>{}</TypeArn>\n <Status>{}</Status>\n <HookStatusReason>{}</HookStatusReason>\n <TargetType>{}</TargetType>\n <TargetId>{}</TargetId>",
2209 xml_escape(r["HookResultId"].as_str().unwrap_or("")),
2210 xml_escape(r["InvocationPoint"].as_str().unwrap_or("PRE_PROVISION")),
2211 xml_escape(r["FailureMode"].as_str().unwrap_or("FAIL")),
2212 xml_escape(r["TypeName"].as_str().unwrap_or("")),
2213 xml_escape(r["TypeVersionId"].as_str().unwrap_or("00000001")),
2214 xml_escape(r["TypeConfigurationVersionId"].as_str().unwrap_or("1")),
2215 xml_escape(r["TypeArn"].as_str().unwrap_or("")),
2216 xml_escape(r["Status"].as_str().unwrap_or("HOOK_COMPLETE_SUCCEEDED")),
2217 xml_escape(r["HookStatusReason"].as_str().unwrap_or("")),
2218 xml_escape(r["TargetType"].as_str().unwrap_or("")),
2219 xml_escape(r["TargetId"].as_str().unwrap_or("")),
2220 )
2221 });
2222 format!(" <HookResults>\n{members}\n </HookResults>")
2223 };
2224 let mut inner = String::new();
2225 if let Some(t) = &target_type {
2226 inner.push_str(&format!(" <TargetType>{}</TargetType>\n", xml_escape(t)));
2227 }
2228 if let Some(t) = &target_id {
2229 inner.push_str(&format!(" <TargetId>{}</TargetId>\n", xml_escape(t)));
2230 }
2231 inner.push_str(&results_xml);
2232 Ok(xml_response("ListHookResults", inner, &rid))
2233 }
2234 "RecordHandlerProgress" => {
2235 require_scalar(¶ms, "BearerToken")?;
2236 require_scalar(¶ms, "OperationStatus")?;
2237 Ok(xml_response_no_result("RecordHandlerProgress", &rid))
2238 }
2239
2240 "ListExports" => {
2242 let accounts = self.state.read();
2243 let mut entries = String::new();
2244 if let Some(state) = accounts.get(&aid) {
2245 for (name, export) in &state.exports {
2246 entries.push_str(&format!(
2247 " <member>\n <ExportingStackId>{}</ExportingStackId>\n <Name>{}</Name>\n <Value>{}</Value>\n </member>\n",
2248 xml_escape(&export.exporting_stack_id),
2249 xml_escape(name),
2250 xml_escape(&export.value),
2251 ));
2252 }
2253 }
2254 let inner = if entries.is_empty() {
2255 " <Exports/>".to_string()
2256 } else {
2257 format!(" <Exports>\n{entries} </Exports>")
2258 };
2259 Ok(xml_response("ListExports", inner, &rid))
2260 }
2261 "ListImports" => {
2262 let export_name = params
2263 .get("ExportName")
2264 .cloned()
2265 .ok_or_else(|| missing("ExportName"))?;
2266 let accounts = self.state.read();
2267 let mut entries = String::new();
2268 if let Some(state) = accounts.get(&aid) {
2269 if let Some(consumers) = state.imports.get(&export_name) {
2270 for stack_name in consumers {
2271 entries.push_str(&format!(
2272 " <member>{}</member>\n",
2273 xml_escape(stack_name)
2274 ));
2275 }
2276 }
2277 }
2278 let inner = if entries.is_empty() {
2279 " <Imports/>".to_string()
2280 } else {
2281 format!(" <Imports>\n{entries} </Imports>")
2282 };
2283 Ok(xml_response("ListImports", inner, &rid))
2284 }
2285
2286 "GetStackPolicy" => {
2288 let stack = params
2289 .get("StackName")
2290 .ok_or_else(|| missing("StackName"))?
2291 .clone();
2292 let accounts = self.state.read();
2293 let body = accounts.get(&aid)
2294 .and_then(|s| s.stack_policies.get(&stack))
2295 .cloned()
2296 .unwrap_or_else(|| r#"{"Statement":[{"Effect":"Allow","Action":"Update:*","Principal":"*","Resource":"*"}]}"#.to_string());
2297 let inner = format!(
2298 " <StackPolicyBody>{}</StackPolicyBody>",
2299 xml_escape(&body)
2300 );
2301 Ok(xml_response("GetStackPolicy", inner, &rid))
2302 }
2303 "SetStackPolicy" => {
2304 let stack = params
2305 .get("StackName")
2306 .ok_or_else(|| missing("StackName"))?
2307 .clone();
2308 let body = params.get("StackPolicyBody").cloned().unwrap_or_default();
2309 let mut accounts = self.state.write();
2310 let state = accounts.get_or_create(&aid);
2311 state.stack_policies.insert(stack, body);
2312 Ok(xml_response_no_result("SetStackPolicy", &rid))
2313 }
2314
2315 "UpdateTerminationProtection" => {
2317 let stack = params
2318 .get("StackName")
2319 .ok_or_else(|| missing("StackName"))?
2320 .clone();
2321 let enabled_raw = params
2322 .get("EnableTerminationProtection")
2323 .ok_or_else(|| missing("EnableTerminationProtection"))?;
2324 let enabled = enabled_raw.eq_ignore_ascii_case("true");
2325 let stack_id = {
2326 let mut accounts = self.state.write();
2327 let state = accounts.get_or_create(&aid);
2328 state.termination_protection.insert(stack.clone(), enabled);
2329 state
2330 .stacks
2331 .get(&stack)
2332 .map(|s| s.stack_id.clone())
2333 .unwrap_or_else(|| stack.clone())
2334 };
2335 Ok(xml_response(
2336 "UpdateTerminationProtection",
2337 format!(" <StackId>{}</StackId>", xml_escape(&stack_id)),
2338 &rid,
2339 ))
2340 }
2341
2342 "DescribeAccountLimits" => Ok(xml_response(
2344 "DescribeAccountLimits",
2345 r#" <AccountLimits>
2346 <member>
2347 <Name>StackLimit</Name>
2348 <Value>2000</Value>
2349 </member>
2350 </AccountLimits>"#
2351 .to_string(),
2352 &rid,
2353 )),
2354 "ActivateOrganizationsAccess" => {
2355 let mut accounts = self.state.write();
2356 let state = accounts.get_or_create(&aid);
2357 state.orgs_access_enabled = true;
2358 Ok(xml_response(
2359 "ActivateOrganizationsAccess",
2360 String::new(),
2361 &rid,
2362 ))
2363 }
2364 "DeactivateOrganizationsAccess" => {
2365 let mut accounts = self.state.write();
2366 let state = accounts.get_or_create(&aid);
2367 state.orgs_access_enabled = false;
2368 Ok(xml_response(
2369 "DeactivateOrganizationsAccess",
2370 String::new(),
2371 &rid,
2372 ))
2373 }
2374 "DescribeOrganizationsAccess" => {
2375 let accounts = self.state.read();
2376 let status = if accounts
2377 .get(&aid)
2378 .map(|s| s.orgs_access_enabled)
2379 .unwrap_or(false)
2380 {
2381 "ENABLED"
2382 } else {
2383 "DISABLED"
2384 };
2385 Ok(xml_response(
2386 "DescribeOrganizationsAccess",
2387 format!(" <Status>{}</Status>", status),
2388 &rid,
2389 ))
2390 }
2391 "ValidateTemplate" => Ok(xml_response(
2392 "ValidateTemplate",
2393 " <Description>Validated</Description>\n <Capabilities/>\n <Parameters/>"
2394 .to_string(),
2395 &rid,
2396 )),
2397 "EstimateTemplateCost" => Ok(xml_response(
2398 "EstimateTemplateCost",
2399 " <Url>https://calculator.aws/#/estimate</Url>".to_string(),
2400 &rid,
2401 )),
2402 "GetTemplateSummary" => Ok(xml_response(
2403 "GetTemplateSummary",
2404 " <Parameters/>\n <ResourceTypes/>\n <Capabilities/>".to_string(),
2405 &rid,
2406 )),
2407 "CancelUpdateStack" => {
2408 params
2409 .get("StackName")
2410 .ok_or_else(|| missing("StackName"))?;
2411 Ok(xml_response_no_result("CancelUpdateStack", &rid))
2412 }
2413 "ContinueUpdateRollback" => {
2414 params
2415 .get("StackName")
2416 .ok_or_else(|| missing("StackName"))?;
2417 Ok(xml_response("ContinueUpdateRollback", String::new(), &rid))
2418 }
2419 "RollbackStack" => {
2420 let stack = params
2421 .get("StackName")
2422 .ok_or_else(|| missing("StackName"))?
2423 .clone();
2424 let stack_id = {
2425 let accounts = self.state.read();
2426 accounts
2427 .get(&aid)
2428 .and_then(|s| s.stacks.get(&stack))
2429 .map(|s| s.stack_id.clone())
2430 .unwrap_or_else(|| stack.clone())
2431 };
2432 Ok(xml_response(
2433 "RollbackStack",
2434 format!(" <StackId>{}</StackId>", xml_escape(&stack_id)),
2435 &rid,
2436 ))
2437 }
2438 "SignalResource" => {
2439 require_scalar(¶ms, "StackName")?;
2440 require_scalar(¶ms, "LogicalResourceId")?;
2441 require_scalar(¶ms, "UniqueId")?;
2442 require_scalar(¶ms, "Status")?;
2443 Ok(xml_response_no_result("SignalResource", &rid))
2444 }
2445
2446 _ => Err(AwsServiceError::action_not_implemented(
2447 "cloudformation",
2448 &action,
2449 )),
2450 }
2451 }
2452}
2453
2454#[cfg(test)]
2455mod tests {
2456 use super::parse_s3_url;
2457 use crate::service::{CloudFormationDeps, CloudFormationService};
2458 use crate::state::{CloudFormationState, SharedCloudFormationState};
2459 use fakecloud_core::delivery::DeliveryBus;
2460 use fakecloud_core::multi_account::MultiAccountState;
2461 use fakecloud_core::service::AwsRequest;
2462 use http::Method;
2463 use parking_lot::RwLock;
2464 use std::collections::HashMap;
2465 use std::sync::Arc;
2466
2467 #[test]
2468 fn parse_s3_url_handles_path_and_virtual_hosted() {
2469 assert_eq!(
2472 parse_s3_url("http://127.0.0.1:4566/bucket/deploy/template.json"),
2473 Some(("bucket".to_string(), "deploy/template.json".to_string()))
2474 );
2475 assert_eq!(
2477 parse_s3_url("https://s3.us-east-1.amazonaws.com/my-bucket/key.yaml"),
2478 Some(("my-bucket".to_string(), "key.yaml".to_string()))
2479 );
2480 assert_eq!(
2482 parse_s3_url("https://my-bucket.s3.amazonaws.com/key.yaml"),
2483 Some(("my-bucket".to_string(), "key.yaml".to_string()))
2484 );
2485 assert_eq!(
2487 parse_s3_url("https://s3.amazonaws.com/b/k.json?versionId=abc"),
2488 Some(("b".to_string(), "k.json".to_string()))
2489 );
2490 assert_eq!(parse_s3_url("https://s3.amazonaws.com/bucket-only"), None);
2492 }
2493
2494 fn deps() -> CloudFormationDeps {
2495 use fakecloud_dynamodb::DynamoDbState;
2496 use fakecloud_ecr::EcrState;
2497 use fakecloud_eventbridge::EventBridgeState;
2498 use fakecloud_iam::IamState;
2499 use fakecloud_kinesis::KinesisState;
2500 use fakecloud_kms::KmsState;
2501 use fakecloud_lambda::LambdaState;
2502 use fakecloud_logs::LogsState;
2503 use fakecloud_s3::S3State;
2504 use fakecloud_secretsmanager::SecretsManagerState;
2505 use fakecloud_sns::SnsState;
2506 use fakecloud_sqs::SqsState;
2507 use fakecloud_ssm::SsmState;
2508
2509 fn shared<T: fakecloud_core::multi_account::AccountState>(
2510 ) -> Arc<RwLock<MultiAccountState<T>>> {
2511 Arc::new(RwLock::new(MultiAccountState::<T>::new(
2512 "000000000000",
2513 "us-east-1",
2514 "",
2515 )))
2516 }
2517 CloudFormationDeps {
2518 sqs: shared::<SqsState>(),
2519 sns: shared::<SnsState>(),
2520 ssm: shared::<SsmState>(),
2521 iam: shared::<IamState>(),
2522 s3: shared::<S3State>(),
2523 eventbridge: shared::<EventBridgeState>(),
2524 dynamodb: shared::<DynamoDbState>(),
2525 logs: shared::<LogsState>(),
2526 lambda: shared::<LambdaState>(),
2527 secretsmanager: shared::<SecretsManagerState>(),
2528 kinesis: shared::<KinesisState>(),
2529 kms: shared::<KmsState>(),
2530 ecr: shared::<EcrState>(),
2531 cloudwatch: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
2532 elbv2: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
2533 organizations: Arc::new(RwLock::new(None)),
2534 cognito: shared::<fakecloud_cognito::CognitoState>(),
2535 rds: shared::<fakecloud_rds::RdsState>(),
2536 ecs: shared::<fakecloud_ecs::EcsState>(),
2537 acm: Arc::new(RwLock::new(fakecloud_acm::AcmAccounts::new())),
2538 elasticache: shared::<fakecloud_elasticache::ElastiCacheState>(),
2539 route53: Arc::new(RwLock::new(fakecloud_route53::Route53Accounts::new())),
2540 cloudfront: Arc::new(RwLock::new(fakecloud_cloudfront::CloudFrontAccounts::new())),
2541 stepfunctions: shared::<fakecloud_stepfunctions::StepFunctionsState>(),
2542 wafv2: Arc::new(RwLock::new(fakecloud_wafv2::Wafv2Accounts::default())),
2543 apigateway: shared::<fakecloud_apigateway::ApiGatewayState>(),
2544 apigatewayv2: shared::<fakecloud_apigatewayv2::ApiGatewayV2State>(),
2545 ses: shared::<fakecloud_ses::SesState>(),
2546 application_autoscaling: Arc::new(parking_lot::RwLock::new(
2547 fakecloud_application_autoscaling::ApplicationAutoScalingAccounts::new(),
2548 )),
2549 athena: Arc::new(parking_lot::RwLock::new(
2550 fakecloud_athena::AthenaAccounts::new(),
2551 )),
2552 firehose: Arc::new(parking_lot::RwLock::new(
2553 fakecloud_firehose::FirehoseAccounts::new(),
2554 )),
2555 glue: Arc::new(parking_lot::RwLock::new(fakecloud_glue::GlueAccounts::new())),
2556 delivery: Arc::new(DeliveryBus::new()),
2557 lambda_runtime: None,
2558 }
2559 }
2560
2561 fn svc() -> CloudFormationService {
2562 let state: SharedCloudFormationState =
2563 Arc::new(RwLock::new(MultiAccountState::<CloudFormationState>::new(
2564 "000000000000",
2565 "us-east-1",
2566 "",
2567 )));
2568 CloudFormationService::new(state, deps())
2569 }
2570
2571 fn req(action: &str, params: &[(&str, &str)]) -> AwsRequest {
2572 let mut q = HashMap::new();
2573 q.insert("Action".to_string(), action.to_string());
2574 for (k, v) in params {
2575 q.insert(k.to_string(), v.to_string());
2576 }
2577 AwsRequest {
2578 service: "cloudformation".to_string(),
2579 method: Method::POST,
2580 raw_path: "/".to_string(),
2581 raw_query: String::new(),
2582 path_segments: vec![],
2583 query_params: q,
2584 headers: http::HeaderMap::new(),
2585 body: bytes::Bytes::new(),
2586 body_stream: parking_lot::Mutex::new(None),
2587 account_id: "000000000000".to_string(),
2588 region: "us-east-1".to_string(),
2589 request_id: "rid".to_string(),
2590 action: action.to_string(),
2591 is_query_protocol: true,
2592 access_key_id: None,
2593 principal: None,
2594 }
2595 }
2596
2597 fn ok(action: &str, params: &[(&str, &str)]) {
2598 let r = svc().handle_extra_action(&req(action, params));
2599 match r {
2600 Ok(resp) => assert!(resp.status.is_success(), "{action} status: {}", resp.status),
2601 Err(e) => panic!("{action} failed: {e:?}"),
2602 }
2603 }
2604
2605 #[test]
2606 fn change_sets() {
2607 ok(
2608 "CreateChangeSet",
2609 &[("StackName", "s"), ("ChangeSetName", "cs")],
2610 );
2611 ok("DescribeChangeSet", &[("ChangeSetName", "cs")]);
2612 ok("DescribeChangeSetHooks", &[("ChangeSetName", "cs")]);
2613 ok("ListChangeSets", &[("StackName", "s")]);
2614 ok("ExecuteChangeSet", &[("ChangeSetName", "cs")]);
2615 ok("DeleteChangeSet", &[("ChangeSetName", "cs")]);
2616 }
2617
2618 fn body_str(resp: &fakecloud_core::service::AwsResponse) -> String {
2619 String::from_utf8(resp.body.expect_bytes().to_vec()).unwrap()
2620 }
2621
2622 #[test]
2623 fn hook_round_trip() {
2624 let s = svc();
2629
2630 s.handle_extra_action(&req(
2632 "SetTypeConfiguration",
2633 &[
2634 ("Type", "HOOK"),
2635 ("TypeName", "MyOrg::MyHook::Hook"),
2636 (
2637 "Configuration",
2638 r#"{"CloudFormationConfiguration":{"HookConfiguration":{"FailureMode":"FAIL"}}}"#,
2639 ),
2640 ],
2641 ))
2642 .expect("SetTypeConfiguration");
2643
2644 s.handle_extra_action(&req(
2647 "CreateChangeSet",
2648 &[
2649 ("StackName", "hooked-stack"),
2650 ("ChangeSetName", "cs1"),
2651 ("ChangeSetType", "CREATE"),
2652 ],
2653 ))
2654 .expect("CreateChangeSet");
2655
2656 let resp = s
2658 .handle_extra_action(&req("DescribeChangeSetHooks", &[("ChangeSetName", "cs1")]))
2659 .expect("DescribeChangeSetHooks");
2660 let xml = body_str(&resp);
2661 assert!(xml.contains("MyOrg::MyHook::Hook"), "hooks XML: {xml}");
2662 assert!(xml.contains("<FailureMode>FAIL</FailureMode>"));
2663
2664 s.handle_extra_action(&req("ExecuteChangeSet", &[("ChangeSetName", "cs1")]))
2666 .expect("ExecuteChangeSet");
2667
2668 let resp = s
2670 .handle_extra_action(&req(
2671 "ListHookResults",
2672 &[("TargetType", "CLOUD_FORMATION")],
2673 ))
2674 .expect("ListHookResults");
2675 let xml = body_str(&resp);
2676 assert!(xml.contains("<HookResults>"), "list XML: {xml}");
2677 assert!(xml.contains("MyOrg::MyHook::Hook"));
2678 assert!(xml.contains("HOOK_COMPLETE_SUCCEEDED"));
2680
2681 let id = xml
2683 .split("<HookResultId>")
2684 .nth(1)
2685 .and_then(|s| s.split("</HookResultId>").next())
2686 .expect("a HookResultId in the list")
2687 .to_string();
2688 let resp = s
2689 .handle_extra_action(&req("GetHookResult", &[("HookResultId", &id)]))
2690 .expect("GetHookResult");
2691 let xml = body_str(&resp);
2692 assert!(xml.contains("HOOK_COMPLETE_SUCCEEDED"), "get XML: {xml}");
2693 assert!(xml.contains("MyOrg::MyHook::Hook"));
2694
2695 let resp = s
2700 .handle_extra_action(&req("GetHookResult", &[("HookResultId", "nope")]))
2701 .expect("GetHookResult for an unknown id still returns 2xx");
2702 let xml = body_str(&resp);
2703 assert!(
2704 xml.contains("<HookResultId>nope</HookResultId>"),
2705 "unknown-id XML: {xml}"
2706 );
2707 assert!(
2708 !xml.contains("MyOrg::MyHook::Hook"),
2709 "unknown id must not echo the recorded hook: {xml}"
2710 );
2711 }
2712
2713 const CS_TEMPLATE: &str = r#"{"Resources":{"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"cs-q"}}},"Outputs":{"QUrl":{"Value":{"Ref":"Q"}}}}"#;
2716
2717 #[test]
2721 fn create_change_set_records_review_in_progress_event() {
2722 let svc = svc();
2723 svc.handle_extra_action(&req(
2724 "CreateChangeSet",
2725 &[
2726 ("StackName", "cs-events"),
2727 ("ChangeSetName", "cs1"),
2728 ("ChangeSetType", "CREATE"),
2729 ("TemplateBody", CS_TEMPLATE),
2730 ],
2731 ))
2732 .expect("create change set");
2733
2734 {
2736 let accounts = svc.state.read();
2737 let acct = accounts.get("000000000000").unwrap();
2738 let total: usize = acct.events.values().map(|v| v.len()).sum();
2739 assert_eq!(total, 1, "expected one event after CreateChangeSet");
2740 let ev = acct.events.values().next().unwrap().last().unwrap();
2741 assert_eq!(ev["ResourceStatus"].as_str(), Some("REVIEW_IN_PROGRESS"));
2742 assert_eq!(
2743 ev["ResourceType"].as_str(),
2744 Some("AWS::CloudFormation::Stack")
2745 );
2746 }
2747
2748 let resp = svc
2750 .handle_extra_action(&req("DescribeStackEvents", &[("StackName", "cs-events")]))
2751 .expect("describe stack events");
2752 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
2753 assert!(!body.contains("<StackEvents/>"), "events list was empty");
2754 assert!(body.contains("REVIEW_IN_PROGRESS"), "body: {body}");
2755 }
2756
2757 #[test]
2762 fn changeset_stack_events_have_monotonic_subsecond_timestamps() {
2763 let svc = svc();
2764 svc.handle_extra_action(&req(
2765 "CreateChangeSet",
2766 &[
2767 ("StackName", "cs-fast"),
2768 ("ChangeSetName", "cs1"),
2769 ("ChangeSetType", "CREATE"),
2770 ("TemplateBody", CS_TEMPLATE),
2771 ],
2772 ))
2773 .expect("create change set");
2774 svc.handle_extra_action(&req(
2775 "ExecuteChangeSet",
2776 &[("StackName", "cs-fast"), ("ChangeSetName", "cs1")],
2777 ))
2778 .expect("execute change set");
2779
2780 let accounts = svc.state.read();
2781 let acct = accounts.get("000000000000").unwrap();
2782 let stack_id = acct.stacks.get("cs-fast").unwrap().stack_id.clone();
2783 let events = acct.events.get(&stack_id).expect("stack has events");
2784
2785 assert!(events.len() >= 3, "expected several lifecycle events");
2788 let ts: Vec<&str> = events
2789 .iter()
2790 .map(|e| e["Timestamp"].as_str().unwrap())
2791 .collect();
2792 assert_eq!(
2793 events.first().unwrap()["ResourceStatus"].as_str(),
2794 Some("REVIEW_IN_PROGRESS")
2795 );
2796 assert_eq!(
2797 events.last().unwrap()["ResourceStatus"].as_str(),
2798 Some("CREATE_COMPLETE")
2799 );
2800 for t in &ts {
2803 assert!(t.contains('.'), "timestamp lacks sub-second precision: {t}");
2804 }
2805 for w in ts.windows(2) {
2806 assert!(w[1] > w[0], "timestamps not strictly increasing: {w:?}");
2807 }
2808 }
2809
2810 #[test]
2814 fn execute_change_set_persists_tags_and_outputs() {
2815 let svc = svc();
2816 svc.handle_extra_action(&req(
2817 "CreateChangeSet",
2818 &[
2819 ("StackName", "cs-stack"),
2820 ("ChangeSetName", "cs1"),
2821 ("ChangeSetType", "CREATE"),
2822 ("TemplateBody", CS_TEMPLATE),
2823 ("Tags.member.1.Key", "ManagedStackSource"),
2824 ("Tags.member.1.Value", "AwsSamCli"),
2825 ],
2826 ))
2827 .expect("create change set");
2828 svc.handle_extra_action(&req(
2829 "ExecuteChangeSet",
2830 &[("StackName", "cs-stack"), ("ChangeSetName", "cs1")],
2831 ))
2832 .expect("execute change set");
2833
2834 let accounts = svc.state.read();
2835 let stack = accounts
2836 .get("000000000000")
2837 .unwrap()
2838 .stacks
2839 .get("cs-stack")
2840 .unwrap();
2841 assert_eq!(stack.status, "CREATE_COMPLETE");
2842 assert_eq!(
2843 stack.tags.get("ManagedStackSource").map(String::as_str),
2844 Some("AwsSamCli"),
2845 "changeset Tags dropped on execute"
2846 );
2847 assert_eq!(stack.outputs.len(), 1, "changeset Outputs not resolved");
2848 assert_eq!(stack.outputs[0].key, "QUrl");
2849 assert!(!stack.outputs[0].value.is_empty());
2850 }
2851
2852 #[test]
2857 fn changeset_provisions_lambda_before_referencing_state_machine() {
2858 let d = deps();
2859 let sfn = d.stepfunctions.clone();
2860 let state: SharedCloudFormationState =
2861 Arc::new(RwLock::new(MultiAccountState::<CloudFormationState>::new(
2862 "000000000000",
2863 "us-east-1",
2864 "",
2865 )));
2866 let svc = CloudFormationService::new(state, d);
2867
2868 let template = r#"{
2871 "Resources": {
2872 "Machine": {
2873 "Type": "AWS::StepFunctions::StateMachine",
2874 "Properties": {
2875 "RoleArn": "arn:aws:iam::000000000000:role/sfn",
2876 "DefinitionString": "{\"StartAt\":\"T\",\"States\":{\"T\":{\"Type\":\"Task\",\"Resource\":\"${fn}\",\"End\":true}}}",
2877 "DefinitionSubstitutions": {"fn": {"Ref": "Worker"}}
2878 }
2879 },
2880 "Worker": {
2881 "Type": "AWS::Lambda::Function",
2882 "Properties": {
2883 "FunctionName": "workflow_dispatcher_v2-1",
2884 "Runtime": "python3.12",
2885 "Handler": "index.handler",
2886 "Role": "arn:aws:iam::000000000000:role/lambda",
2887 "Code": {"ZipFile": "def handler(e, c): return e"}
2888 }
2889 }
2890 }
2891 }"#;
2892
2893 svc.handle_extra_action(&req(
2894 "CreateChangeSet",
2895 &[
2896 ("StackName", "wf"),
2897 ("ChangeSetName", "cs1"),
2898 ("ChangeSetType", "CREATE"),
2899 ("TemplateBody", template),
2900 ],
2901 ))
2902 .expect("create change set");
2903 svc.handle_extra_action(&req(
2904 "ExecuteChangeSet",
2905 &[("StackName", "wf"), ("ChangeSetName", "cs1")],
2906 ))
2907 .expect("execute change set");
2908
2909 let accounts = sfn.read();
2910 let st = accounts.get("000000000000").expect("sfn account exists");
2911 let machine = st
2912 .state_machines
2913 .values()
2914 .next()
2915 .expect("state machine was provisioned");
2916 assert!(
2917 machine.definition.contains("workflow_dispatcher_v2-1"),
2918 "ASL should carry the resolved function name, got: {}",
2919 machine.definition
2920 );
2921 assert!(
2922 !machine.definition.contains("${fn}"),
2923 "substitution token left unreplaced: {}",
2924 machine.definition
2925 );
2926 assert!(
2927 !machine.definition.contains("Worker"),
2928 "logical id leaked into baked ASL: {}",
2929 machine.definition
2930 );
2931 }
2932
2933 #[test]
2934 fn stack_sets_instances_refactors() {
2935 ok("CreateStackSet", &[("StackSetName", "ss")]);
2936 ok("DescribeStackSet", &[("StackSetName", "ss")]);
2937 ok("ListStackSets", &[]);
2938 ok("UpdateStackSet", &[("StackSetName", "ss")]);
2939 ok(
2940 "DescribeStackSetOperation",
2941 &[("StackSetName", "ss"), ("OperationId", "op")],
2942 );
2943 ok("ListStackSetOperations", &[("StackSetName", "ss")]);
2944 ok(
2945 "ListStackSetOperationResults",
2946 &[("StackSetName", "ss"), ("OperationId", "op")],
2947 );
2948 ok(
2949 "ListStackSetAutoDeploymentTargets",
2950 &[("StackSetName", "ss")],
2951 );
2952 ok(
2953 "StopStackSetOperation",
2954 &[("StackSetName", "ss"), ("OperationId", "op")],
2955 );
2956 ok("ImportStacksToStackSet", &[("StackSetName", "ss")]);
2957 ok("DeleteStackSet", &[("StackSetName", "ss")]);
2958 ok(
2959 "CreateStackInstances",
2960 &[("StackSetName", "ss"), ("Regions.member.1", "us-east-1")],
2961 );
2962 ok(
2963 "UpdateStackInstances",
2964 &[("StackSetName", "ss"), ("Regions.member.1", "us-east-1")],
2965 );
2966 ok(
2967 "DeleteStackInstances",
2968 &[
2969 ("StackSetName", "ss"),
2970 ("Regions.member.1", "us-east-1"),
2971 ("RetainStacks", "false"),
2972 ],
2973 );
2974 ok(
2975 "DescribeStackInstance",
2976 &[
2977 ("StackSetName", "ss"),
2978 ("StackInstanceAccount", "000000000000"),
2979 ("StackInstanceRegion", "us-east-1"),
2980 ],
2981 );
2982 ok("ListStackInstances", &[("StackSetName", "ss")]);
2983 ok(
2984 "ListStackInstanceResourceDrifts",
2985 &[
2986 ("StackSetName", "ss"),
2987 ("StackInstanceAccount", "000000000000"),
2988 ("StackInstanceRegion", "us-east-1"),
2989 ("OperationId", "op"),
2990 ],
2991 );
2992 ok(
2993 "CreateStackRefactor",
2994 &[("StackDefinitions.member.1.StackName", "s")],
2995 );
2996 ok("DescribeStackRefactor", &[("StackRefactorId", "r")]);
2997 ok("ExecuteStackRefactor", &[("StackRefactorId", "r")]);
2998 ok("ListStackRefactors", &[]);
2999 ok("ListStackRefactorActions", &[("StackRefactorId", "r")]);
3000 }
3001
3002 #[test]
3003 fn types_and_publishers() {
3004 ok("ActivateType", &[]);
3005 ok("DeactivateType", &[]);
3006 ok("DescribeType", &[]);
3007 ok("DescribeTypeRegistration", &[("RegistrationToken", "tok")]);
3008 ok(
3009 "RegisterType",
3010 &[("TypeName", "T"), ("SchemaHandlerPackage", "pkg")],
3011 );
3012 ok("DeregisterType", &[]);
3013 ok("ListTypes", &[]);
3014 ok("ListTypeRegistrations", &[]);
3015 ok("ListTypeVersions", &[]);
3016 ok(
3017 "BatchDescribeTypeConfigurations",
3018 &[("TypeConfigurationIdentifiers.member.1.Type", "RESOURCE")],
3019 );
3020 ok("SetTypeConfiguration", &[("Configuration", "{}")]);
3021 ok("SetTypeDefaultVersion", &[]);
3022 ok("TestType", &[]);
3023 ok("PublishType", &[]);
3024 ok("RegisterPublisher", &[]);
3025 ok("DescribePublisher", &[]);
3026 }
3027
3028 #[test]
3029 fn templates_resource_scans_drift() {
3030 ok(
3031 "CreateGeneratedTemplate",
3032 &[("GeneratedTemplateName", "gt")],
3033 );
3034 ok(
3035 "UpdateGeneratedTemplate",
3036 &[("GeneratedTemplateName", "gt")],
3037 );
3038 ok(
3039 "DescribeGeneratedTemplate",
3040 &[("GeneratedTemplateName", "gt")],
3041 );
3042 ok("GetGeneratedTemplate", &[("GeneratedTemplateName", "gt")]);
3043 ok("ListGeneratedTemplates", &[]);
3044 ok(
3045 "DeleteGeneratedTemplate",
3046 &[("GeneratedTemplateName", "gt")],
3047 );
3048 ok("StartResourceScan", &[]);
3049 ok("DescribeResourceScan", &[("ResourceScanId", "rs")]);
3050 ok("ListResourceScans", &[]);
3051 ok("ListResourceScanResources", &[("ResourceScanId", "rs")]);
3052 ok(
3053 "ListResourceScanRelatedResources",
3054 &[
3055 ("ResourceScanId", "rs"),
3056 ("Resources.member.1.ResourceType", "AWS::SQS::Queue"),
3057 ],
3058 );
3059 ok("DetectStackDrift", &[("StackName", "s")]);
3060 ok(
3061 "DetectStackResourceDrift",
3062 &[("StackName", "s"), ("LogicalResourceId", "L")],
3063 );
3064 ok("DetectStackSetDrift", &[("StackSetName", "ss")]);
3065 ok(
3066 "DescribeStackDriftDetectionStatus",
3067 &[("StackDriftDetectionId", "id")],
3068 );
3069 ok("DescribeStackResourceDrifts", &[("StackName", "s")]);
3070 ok(
3071 "DescribeStackResource",
3072 &[("StackName", "s"), ("LogicalResourceId", "L")],
3073 );
3074 }
3075
3076 #[test]
3077 fn events_hooks_imports_policies_org() {
3078 ok("DescribeStackEvents", &[("StackName", "s")]);
3079 ok("DescribeEvents", &[]);
3080 ok("ListHookResults", &[]);
3085 ok(
3086 "RecordHandlerProgress",
3087 &[("BearerToken", "tok"), ("OperationStatus", "SUCCESS")],
3088 );
3089 ok("ListExports", &[]);
3090 ok("ListImports", &[("ExportName", "SomeExport")]);
3091 ok("GetStackPolicy", &[("StackName", "s")]);
3092 ok("SetStackPolicy", &[("StackName", "s")]);
3093 ok(
3094 "UpdateTerminationProtection",
3095 &[("StackName", "s"), ("EnableTerminationProtection", "false")],
3096 );
3097 ok("DescribeAccountLimits", &[]);
3098 ok("ActivateOrganizationsAccess", &[]);
3099 ok("DescribeOrganizationsAccess", &[]);
3100 ok("DeactivateOrganizationsAccess", &[]);
3101 ok("ValidateTemplate", &[]);
3102 ok("EstimateTemplateCost", &[]);
3103 ok("GetTemplateSummary", &[]);
3104 ok("CancelUpdateStack", &[("StackName", "s")]);
3105 ok("ContinueUpdateRollback", &[("StackName", "s")]);
3106 ok("RollbackStack", &[("StackName", "s")]);
3107 ok(
3108 "SignalResource",
3109 &[
3110 ("StackName", "s"),
3111 ("LogicalResourceId", "L"),
3112 ("UniqueId", "U"),
3113 ("Status", "SUCCESS"),
3114 ],
3115 );
3116 }
3117}