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