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