1use async_trait::async_trait;
2use chrono::Utc;
3use http::StatusCode;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use fakecloud_core::delivery::DeliveryBus;
8use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
9use fakecloud_dynamodb::state::SharedDynamoDbState;
10use fakecloud_eventbridge::state::SharedEventBridgeState;
11use fakecloud_iam::state::SharedIamState;
12use fakecloud_logs::state::SharedLogsState;
13use fakecloud_persistence::SnapshotStore;
14use fakecloud_s3::state::SharedS3State;
15use fakecloud_sns::state::SharedSnsState;
16use fakecloud_sqs::state::SharedSqsState;
17use fakecloud_ssm::state::SharedSsmState;
18use tokio::sync::Mutex as AsyncMutex;
19
20use crate::resource_provisioner::ResourceProvisioner;
21use crate::state::{
22 CloudFormationSnapshot, CloudFormationState, SharedCloudFormationState, Stack, StackResource,
23 CLOUDFORMATION_SNAPSHOT_SCHEMA_VERSION,
24};
25use crate::template;
26use crate::xml_responses;
27
28fn provision_stack_resources(
37 provisioner: &ResourceProvisioner,
38 resource_defs: &[template::ResourceDefinition],
39 template_body: &str,
40 parameters: &HashMap<String, String>,
41) -> Result<Vec<StackResource>, AwsServiceError> {
42 let mut resources = Vec::new();
43 let mut physical_ids: HashMap<String, String> = HashMap::new();
44 let mut pending: Vec<&template::ResourceDefinition> = resource_defs.iter().collect();
45 let max_passes = pending.len() + 1;
46
47 for _ in 0..max_passes {
48 if pending.is_empty() {
49 break;
50 }
51 let mut still_pending = Vec::new();
52 let mut made_progress = false;
53
54 for resource_def in pending {
55 let resolved_def = template::resolve_resource_properties(
56 resource_def,
57 template_body,
58 parameters,
59 &physical_ids,
60 )
61 .map_err(|e| {
62 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationError", e)
63 })?;
64
65 match provisioner.create_resource(&resolved_def) {
66 Ok(stack_resource) => {
67 physical_ids.insert(
68 stack_resource.logical_id.clone(),
69 stack_resource.physical_id.clone(),
70 );
71 resources.push(stack_resource);
72 made_progress = true;
73 }
74 Err(_) => still_pending.push(resource_def),
75 }
76 }
77
78 pending = still_pending;
79 if !made_progress && !pending.is_empty() {
80 let resource_def = pending[0];
83 let resolved_def = template::resolve_resource_properties(
84 resource_def,
85 template_body,
86 parameters,
87 &physical_ids,
88 )
89 .unwrap_or_else(|_| resource_def.clone());
90 let err = provisioner.create_resource(&resolved_def).unwrap_err();
91 for r in &resources {
92 let _ = provisioner.delete_resource(r);
93 }
94 return Err(AwsServiceError::aws_error(
95 StatusCode::BAD_REQUEST,
96 "ValidationError",
97 format!(
98 "Failed to create resource {}: {err}",
99 resource_def.logical_id
100 ),
101 ));
102 }
103 }
104
105 Ok(resources)
106}
107
108pub struct CloudFormationDeps {
110 pub sqs: SharedSqsState,
111 pub sns: SharedSnsState,
112 pub ssm: SharedSsmState,
113 pub iam: SharedIamState,
114 pub s3: SharedS3State,
115 pub eventbridge: SharedEventBridgeState,
116 pub dynamodb: SharedDynamoDbState,
117 pub logs: SharedLogsState,
118 pub delivery: Arc<DeliveryBus>,
119}
120
121pub struct CloudFormationService {
122 pub(crate) state: SharedCloudFormationState,
123 deps: CloudFormationDeps,
124 snapshot_store: Option<Arc<dyn SnapshotStore>>,
125 snapshot_lock: Arc<AsyncMutex<()>>,
126}
127
128impl CloudFormationService {
129 pub fn new(state: SharedCloudFormationState, deps: CloudFormationDeps) -> Self {
130 Self {
131 state,
132 deps,
133 snapshot_store: None,
134 snapshot_lock: Arc::new(AsyncMutex::new(())),
135 }
136 }
137
138 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
139 self.snapshot_store = Some(store);
140 self
141 }
142
143 async fn save_snapshot(&self) {
144 let Some(store) = self.snapshot_store.clone() else {
145 return;
146 };
147 let _guard = self.snapshot_lock.lock().await;
148 let snapshot = CloudFormationSnapshot {
149 schema_version: CLOUDFORMATION_SNAPSHOT_SCHEMA_VERSION,
150 state: None,
151 accounts: Some(self.state.read().clone()),
152 };
153 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
154 let bytes = serde_json::to_vec(&snapshot)
155 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
156 store.save(&bytes)
157 })
158 .await;
159 match join {
160 Ok(Ok(())) => {}
161 Ok(Err(err)) => tracing::error!(%err, "failed to write cloudformation snapshot"),
162 Err(err) => tracing::error!(%err, "cloudformation snapshot task panicked"),
163 }
164 }
165
166 fn provisioner(&self, stack_id: &str, account_id: &str, region: &str) -> ResourceProvisioner {
167 ResourceProvisioner {
168 sqs_state: self.deps.sqs.clone(),
169 sns_state: self.deps.sns.clone(),
170 ssm_state: self.deps.ssm.clone(),
171 iam_state: self.deps.iam.clone(),
172 s3_state: self.deps.s3.clone(),
173 eventbridge_state: self.deps.eventbridge.clone(),
174 dynamodb_state: self.deps.dynamodb.clone(),
175 logs_state: self.deps.logs.clone(),
176 delivery: self.deps.delivery.clone(),
177 account_id: account_id.to_string(),
178 region: region.to_string(),
179 stack_id: stack_id.to_string(),
180 }
181 }
182
183 fn get_param(req: &AwsRequest, key: &str) -> Option<String> {
184 if let Some(v) = req.query_params.get(key) {
186 return Some(v.clone());
187 }
188 let body_params = fakecloud_core::protocol::parse_query_body(&req.body);
190 body_params.get(key).cloned()
191 }
192
193 pub(crate) fn get_all_params(req: &AwsRequest) -> HashMap<String, String> {
194 let mut params = req.query_params.clone();
195 let body_params = fakecloud_core::protocol::parse_query_body(&req.body);
196 for (k, v) in body_params {
197 params.entry(k).or_insert(v);
198 }
199 params
200 }
201
202 fn extract_tags(params: &HashMap<String, String>) -> HashMap<String, String> {
203 let mut tags = HashMap::new();
204 for i in 1.. {
205 let key_param = format!("Tags.member.{i}.Key");
206 let value_param = format!("Tags.member.{i}.Value");
207 match (params.get(&key_param), params.get(&value_param)) {
208 (Some(k), Some(v)) => {
209 tags.insert(k.clone(), v.clone());
210 }
211 _ => break,
212 }
213 }
214 tags
215 }
216
217 fn extract_parameters(params: &HashMap<String, String>) -> HashMap<String, String> {
218 let mut result = HashMap::new();
219 for i in 1.. {
220 let key_param = format!("Parameters.member.{i}.ParameterKey");
221 let value_param = format!("Parameters.member.{i}.ParameterValue");
222 match (params.get(&key_param), params.get(&value_param)) {
223 (Some(k), Some(v)) => {
224 result.insert(k.clone(), v.clone());
225 }
226 _ => break,
227 }
228 }
229 result
230 }
231
232 fn extract_notification_arns(params: &HashMap<String, String>) -> Vec<String> {
233 let mut arns = Vec::new();
234 for i in 1.. {
235 let key = format!("NotificationARNs.member.{i}");
236 match params.get(&key) {
237 Some(arn) => arns.push(arn.clone()),
238 None => break,
239 }
240 }
241 arns
242 }
243
244 fn send_stack_notification(
245 delivery: &DeliveryBus,
246 notification_arns: &[String],
247 stack_name: &str,
248 stack_id: &str,
249 status: &str,
250 ) {
251 if notification_arns.is_empty() {
252 return;
253 }
254 let message = format!(
255 "StackId='{}'\nTimestamp='{}'\nEventId='{}'\nLogicalResourceId='{}'\nResourceStatus='{}'\nResourceType='AWS::CloudFormation::Stack'\nStackName='{}'",
256 stack_id,
257 chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ"),
258 uuid::Uuid::new_v4(),
259 stack_name,
260 status,
261 stack_name,
262 );
263 for arn in notification_arns {
264 delivery.publish_to_sns(arn, &message, Some("AWS CloudFormation Notification"));
265 }
266 }
267
268 fn create_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
269 let params = Self::get_all_params(req);
270
271 let stack_name = params.get("StackName").ok_or_else(|| {
272 AwsServiceError::aws_error(
273 StatusCode::BAD_REQUEST,
274 "ValidationError",
275 "StackName is required",
276 )
277 })?;
278
279 let template_body = params.get("TemplateBody").ok_or_else(|| {
280 AwsServiceError::aws_error(
281 StatusCode::BAD_REQUEST,
282 "ValidationError",
283 "TemplateBody is required",
284 )
285 })?;
286
287 {
289 let accounts = self.state.read();
290 let empty = CloudFormationState::new(&req.account_id, &req.region);
291 let state = accounts.get(&req.account_id).unwrap_or(&empty);
292 if let Some(existing) = state.stacks.get(stack_name.as_str()) {
293 if existing.status != "DELETE_COMPLETE" {
294 return Err(AwsServiceError::aws_error(
295 StatusCode::BAD_REQUEST,
296 "AlreadyExistsException",
297 format!("Stack [{stack_name}] already exists"),
298 ));
299 }
300 }
301 }
302
303 let tags = Self::extract_tags(¶ms);
304 let parameters = Self::extract_parameters(¶ms);
305 let notification_arns = Self::extract_notification_arns(¶ms);
306
307 let parsed = template::parse_template(template_body, ¶meters).map_err(|e| {
309 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationError", e)
310 })?;
311
312 let stack_id = format!(
313 "arn:aws:cloudformation:{}:{}:stack/{}/{}",
314 req.region,
315 req.account_id,
316 stack_name,
317 uuid::Uuid::new_v4()
318 );
319
320 let provisioner = self.provisioner(&stack_id, &req.account_id, &req.region);
321 let resources =
322 provision_stack_resources(&provisioner, &parsed.resources, template_body, ¶meters)?;
323
324 let stack = Stack {
325 name: stack_name.clone(),
326 stack_id: stack_id.clone(),
327 template: template_body.clone(),
328 status: "CREATE_COMPLETE".to_string(),
329 resources,
330 parameters,
331 tags,
332 created_at: Utc::now(),
333 updated_at: None,
334 description: parsed.description,
335 notification_arns: notification_arns.clone(),
336 };
337
338 {
339 let mut accounts = self.state.write();
340 let state = accounts.get_or_create(&req.account_id);
341 state.stacks.insert(stack_name.clone(), stack);
342 }
343
344 Self::send_stack_notification(
345 &self.deps.delivery,
346 ¬ification_arns,
347 stack_name,
348 &stack_id,
349 "CREATE_COMPLETE",
350 );
351
352 Ok(AwsResponse::xml(
353 StatusCode::OK,
354 xml_responses::create_stack_response(&stack_id, &req.request_id),
355 ))
356 }
357
358 fn delete_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
359 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
360 AwsServiceError::aws_error(
361 StatusCode::BAD_REQUEST,
362 "ValidationError",
363 "StackName is required",
364 )
365 })?;
366
367 let mut accounts = self.state.write();
368 let state = accounts.get_or_create(&req.account_id);
369
370 let stack = state.stacks.values_mut().find(|s| {
372 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
373 });
374
375 if let Some(stack) = stack {
376 let stack_id = stack.stack_id.clone();
377 let stack_name_for_notif = stack.name.clone();
378 let notification_arns = stack.notification_arns.clone();
379 let resources: Vec<_> = stack.resources.clone();
380
381 drop(accounts);
384 let provisioner = self.provisioner(&stack_id, &req.account_id, &req.region);
385
386 for resource in resources.iter().rev() {
388 let _ = provisioner.delete_resource(resource);
389 }
390
391 let mut accounts = self.state.write();
393 let state = accounts.get_or_create(&req.account_id);
394 if let Some(stack) = state.stacks.values_mut().find(|s| s.stack_id == stack_id) {
395 stack.status = "DELETE_COMPLETE".to_string();
396 stack.resources.clear();
397 }
398 drop(accounts);
399
400 Self::send_stack_notification(
401 &self.deps.delivery,
402 ¬ification_arns,
403 &stack_name_for_notif,
404 &stack_id,
405 "DELETE_COMPLETE",
406 );
407 }
408
409 Ok(AwsResponse::xml(
410 StatusCode::OK,
411 xml_responses::delete_stack_response(&req.request_id),
412 ))
413 }
414
415 fn describe_stacks(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
416 let stack_name = Self::get_param(req, "StackName");
417
418 let accounts = self.state.read();
419 let empty = CloudFormationState::new(&req.account_id, &req.region);
420 let state = accounts.get(&req.account_id).unwrap_or(&empty);
421 let stacks: Vec<Stack> = if let Some(ref name) = stack_name {
422 state
423 .stacks
424 .values()
425 .filter(|s| {
426 (s.name == *name || s.stack_id == *name) && s.status != "DELETE_COMPLETE"
427 })
428 .cloned()
429 .collect()
430 } else {
431 state
432 .stacks
433 .values()
434 .filter(|s| s.status != "DELETE_COMPLETE")
435 .cloned()
436 .collect()
437 };
438
439 if let Some(ref name) = stack_name {
440 if stacks.is_empty() {
441 return Err(AwsServiceError::aws_error(
442 StatusCode::BAD_REQUEST,
443 "ValidationError",
444 format!("Stack with id {name} does not exist"),
445 ));
446 }
447 }
448
449 Ok(AwsResponse::xml(
450 StatusCode::OK,
451 xml_responses::describe_stacks_response(&stacks, &req.request_id),
452 ))
453 }
454
455 fn list_stacks(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
456 let accounts = self.state.read();
457 let empty = CloudFormationState::new(&req.account_id, &req.region);
458 let state = accounts.get(&req.account_id).unwrap_or(&empty);
459 let stacks: Vec<Stack> = state.stacks.values().cloned().collect();
460
461 Ok(AwsResponse::xml(
462 StatusCode::OK,
463 xml_responses::list_stacks_response(&stacks, &req.request_id),
464 ))
465 }
466
467 fn list_stack_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
468 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
469 AwsServiceError::aws_error(
470 StatusCode::BAD_REQUEST,
471 "ValidationError",
472 "StackName is required",
473 )
474 })?;
475
476 let accounts = self.state.read();
477 let empty = CloudFormationState::new(&req.account_id, &req.region);
478 let state = accounts.get(&req.account_id).unwrap_or(&empty);
479 let stack = state
480 .stacks
481 .values()
482 .find(|s| {
483 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
484 })
485 .ok_or_else(|| {
486 AwsServiceError::aws_error(
487 StatusCode::BAD_REQUEST,
488 "ValidationError",
489 format!("Stack [{stack_name}] does not exist"),
490 )
491 })?;
492
493 Ok(AwsResponse::xml(
494 StatusCode::OK,
495 xml_responses::list_stack_resources_response(&stack.resources, &req.request_id),
496 ))
497 }
498
499 fn describe_stack_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
500 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
501 AwsServiceError::aws_error(
502 StatusCode::BAD_REQUEST,
503 "ValidationError",
504 "StackName is required",
505 )
506 })?;
507
508 let accounts = self.state.read();
509 let empty = CloudFormationState::new(&req.account_id, &req.region);
510 let state = accounts.get(&req.account_id).unwrap_or(&empty);
511 let stack = state
512 .stacks
513 .values()
514 .find(|s| {
515 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
516 })
517 .ok_or_else(|| {
518 AwsServiceError::aws_error(
519 StatusCode::BAD_REQUEST,
520 "ValidationError",
521 format!("Stack [{stack_name}] does not exist"),
522 )
523 })?;
524
525 Ok(AwsResponse::xml(
526 StatusCode::OK,
527 xml_responses::describe_stack_resources_response(
528 &stack.resources,
529 &stack.name,
530 &req.request_id,
531 ),
532 ))
533 }
534
535 fn update_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
536 let input = UpdateStackInput::from_params(req)?;
537 let parsed =
538 template::parse_template(&input.template_body, &input.parameters).map_err(|e| {
539 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationError", e)
540 })?;
541
542 let found_stack_id = {
544 let accounts = self.state.read();
545 let empty = CloudFormationState::new(&req.account_id, &req.region);
546 let state = accounts.get(&req.account_id).unwrap_or(&empty);
547 state
548 .stacks
549 .values()
550 .find(|s| {
551 (s.name == input.stack_name || s.stack_id == input.stack_name)
552 && s.status != "DELETE_COMPLETE"
553 })
554 .map(|s| s.stack_id.clone())
555 .unwrap_or_default()
556 };
557
558 let provisioner = self.provisioner(&found_stack_id, &req.account_id, &req.region);
559
560 let mut accounts = self.state.write();
561 let state = accounts.get_or_create(&req.account_id);
562 let stack = state
563 .stacks
564 .values_mut()
565 .find(|s| {
566 (s.name == input.stack_name || s.stack_id == input.stack_name)
567 && s.status != "DELETE_COMPLETE"
568 })
569 .ok_or_else(|| {
570 AwsServiceError::aws_error(
571 StatusCode::BAD_REQUEST,
572 "ValidationError",
573 format!("Stack [{}] does not exist", input.stack_name),
574 )
575 })?;
576
577 let update_result = apply_resource_updates(
578 stack,
579 &parsed.resources,
580 &input.template_body,
581 &input.parameters,
582 &provisioner,
583 );
584
585 let stack_id = stack.stack_id.clone();
586 stack.template = input.template_body.clone();
587 stack.status = if update_result.is_err() {
588 "UPDATE_FAILED".to_string()
589 } else {
590 "UPDATE_COMPLETE".to_string()
591 };
592 stack.parameters = input.parameters;
593 if !input.tags.is_empty() {
594 stack.tags = input.tags;
595 }
596 stack.updated_at = Some(Utc::now());
597 stack.description = parsed.description;
598 if !input.notification_arns.is_empty() {
599 stack.notification_arns = input.notification_arns.clone();
600 }
601 let notification_arns = stack.notification_arns.clone();
602 let stack_name_for_notif = stack.name.clone();
603
604 if let Err(error_msg) = update_result {
605 drop(accounts);
606 Self::send_stack_notification(
607 &self.deps.delivery,
608 ¬ification_arns,
609 &stack_name_for_notif,
610 &stack_id,
611 "UPDATE_FAILED",
612 );
613 return Err(AwsServiceError::aws_error(
614 StatusCode::BAD_REQUEST,
615 "ValidationError",
616 error_msg,
617 ));
618 }
619
620 drop(accounts);
621 Self::send_stack_notification(
622 &self.deps.delivery,
623 ¬ification_arns,
624 &stack_name_for_notif,
625 &stack_id,
626 "UPDATE_COMPLETE",
627 );
628
629 Ok(AwsResponse::xml(
630 StatusCode::OK,
631 xml_responses::update_stack_response(&stack_id, &req.request_id),
632 ))
633 }
634
635 fn get_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
636 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
637 AwsServiceError::aws_error(
638 StatusCode::BAD_REQUEST,
639 "ValidationError",
640 "StackName is required",
641 )
642 })?;
643
644 let accounts = self.state.read();
645 let empty = CloudFormationState::new(&req.account_id, &req.region);
646 let state = accounts.get(&req.account_id).unwrap_or(&empty);
647 let stack = state
648 .stacks
649 .values()
650 .find(|s| {
651 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
652 })
653 .ok_or_else(|| {
654 AwsServiceError::aws_error(
655 StatusCode::BAD_REQUEST,
656 "ValidationError",
657 format!("Stack [{stack_name}] does not exist"),
658 )
659 })?;
660
661 Ok(AwsResponse::xml(
662 StatusCode::OK,
663 xml_responses::get_template_response(&stack.template, &req.request_id),
664 ))
665 }
666}
667
668#[async_trait]
669impl AwsService for CloudFormationService {
670 fn service_name(&self) -> &str {
671 "cloudformation"
672 }
673
674 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
675 let action = req.action.as_str();
676 let mutates = matches!(
680 action,
681 "CreateStack"
682 | "DeleteStack"
683 | "UpdateStack"
684 | "CreateChangeSet"
685 | "DeleteChangeSet"
686 | "CreateStackSet"
687 | "DeleteStackSet"
688 | "CreateStackRefactor"
689 | "CreateGeneratedTemplate"
690 | "DeleteGeneratedTemplate"
691 | "SetStackPolicy"
692 | "UpdateTerminationProtection"
693 | "ActivateOrganizationsAccess"
694 | "DeactivateOrganizationsAccess"
695 );
696 let result = match action {
697 "CreateStack" => self.create_stack(&req),
698 "DeleteStack" => self.delete_stack(&req),
699 "DescribeStacks" => self.describe_stacks(&req),
700 "ListStacks" => self.list_stacks(&req),
701 "ListStackResources" => self.list_stack_resources(&req),
702 "DescribeStackResources" => self.describe_stack_resources(&req),
703 "UpdateStack" => self.update_stack(&req),
704 "GetTemplate" => self.get_template(&req),
705 _ => self.handle_extra_action(&req),
706 };
707 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
708 self.save_snapshot().await;
709 }
710 result
711 }
712
713 fn supported_actions(&self) -> &[&str] {
714 &[
715 "ActivateOrganizationsAccess",
716 "ActivateType",
717 "BatchDescribeTypeConfigurations",
718 "CancelUpdateStack",
719 "ContinueUpdateRollback",
720 "CreateChangeSet",
721 "CreateGeneratedTemplate",
722 "CreateStack",
723 "CreateStackInstances",
724 "CreateStackRefactor",
725 "CreateStackSet",
726 "DeactivateOrganizationsAccess",
727 "DeactivateType",
728 "DeleteChangeSet",
729 "DeleteGeneratedTemplate",
730 "DeleteStack",
731 "DeleteStackInstances",
732 "DeleteStackSet",
733 "DeregisterType",
734 "DescribeAccountLimits",
735 "DescribeChangeSet",
736 "DescribeChangeSetHooks",
737 "DescribeEvents",
738 "DescribeGeneratedTemplate",
739 "DescribeOrganizationsAccess",
740 "DescribePublisher",
741 "DescribeResourceScan",
742 "DescribeStackDriftDetectionStatus",
743 "DescribeStackEvents",
744 "DescribeStackInstance",
745 "DescribeStackRefactor",
746 "DescribeStackResource",
747 "DescribeStackResourceDrifts",
748 "DescribeStackResources",
749 "DescribeStackSet",
750 "DescribeStackSetOperation",
751 "DescribeStacks",
752 "DescribeType",
753 "DescribeTypeRegistration",
754 "DetectStackDrift",
755 "DetectStackResourceDrift",
756 "DetectStackSetDrift",
757 "EstimateTemplateCost",
758 "ExecuteChangeSet",
759 "ExecuteStackRefactor",
760 "GetGeneratedTemplate",
761 "GetHookResult",
762 "GetStackPolicy",
763 "GetTemplate",
764 "GetTemplateSummary",
765 "ImportStacksToStackSet",
766 "ListChangeSets",
767 "ListExports",
768 "ListGeneratedTemplates",
769 "ListHookResults",
770 "ListImports",
771 "ListResourceScanRelatedResources",
772 "ListResourceScanResources",
773 "ListResourceScans",
774 "ListStackInstanceResourceDrifts",
775 "ListStackInstances",
776 "ListStackRefactorActions",
777 "ListStackRefactors",
778 "ListStackResources",
779 "ListStackSetAutoDeploymentTargets",
780 "ListStackSetOperationResults",
781 "ListStackSetOperations",
782 "ListStackSets",
783 "ListStacks",
784 "ListTypeRegistrations",
785 "ListTypeVersions",
786 "ListTypes",
787 "PublishType",
788 "RecordHandlerProgress",
789 "RegisterPublisher",
790 "RegisterType",
791 "RollbackStack",
792 "SetStackPolicy",
793 "SetTypeConfiguration",
794 "SetTypeDefaultVersion",
795 "SignalResource",
796 "StartResourceScan",
797 "StopStackSetOperation",
798 "TestType",
799 "UpdateGeneratedTemplate",
800 "UpdateStack",
801 "UpdateStackInstances",
802 "UpdateStackSet",
803 "UpdateTerminationProtection",
804 "ValidateTemplate",
805 ]
806 }
807}
808
809struct UpdateStackInput {
811 stack_name: String,
812 template_body: String,
813 parameters: HashMap<String, String>,
814 tags: HashMap<String, String>,
815 notification_arns: Vec<String>,
816}
817
818impl UpdateStackInput {
819 fn from_params(req: &AwsRequest) -> Result<Self, AwsServiceError> {
820 let params = CloudFormationService::get_all_params(req);
821
822 let stack_name = params
823 .get("StackName")
824 .ok_or_else(|| {
825 AwsServiceError::aws_error(
826 StatusCode::BAD_REQUEST,
827 "ValidationError",
828 "StackName is required",
829 )
830 })?
831 .to_string();
832
833 let template_body = params
834 .get("TemplateBody")
835 .ok_or_else(|| {
836 AwsServiceError::aws_error(
837 StatusCode::BAD_REQUEST,
838 "ValidationError",
839 "TemplateBody is required",
840 )
841 })?
842 .to_string();
843
844 Ok(Self {
845 stack_name,
846 template_body,
847 parameters: CloudFormationService::extract_parameters(¶ms),
848 tags: CloudFormationService::extract_tags(¶ms),
849 notification_arns: CloudFormationService::extract_notification_arns(¶ms),
850 })
851 }
852}
853
854fn apply_resource_updates(
857 stack: &mut crate::state::Stack,
858 new_resource_defs: &[template::ResourceDefinition],
859 template_body: &str,
860 parameters: &HashMap<String, String>,
861 provisioner: &crate::resource_provisioner::ResourceProvisioner,
862) -> Result<(), String> {
863 let old_logical_ids: std::collections::HashSet<String> = stack
864 .resources
865 .iter()
866 .map(|r| r.logical_id.clone())
867 .collect();
868 let new_logical_ids: std::collections::HashSet<String> = new_resource_defs
869 .iter()
870 .map(|r| r.logical_id.clone())
871 .collect();
872
873 let to_remove: Vec<_> = stack
875 .resources
876 .iter()
877 .filter(|r| !new_logical_ids.contains(&r.logical_id))
878 .cloned()
879 .collect();
880 for resource in &to_remove {
881 let _ = provisioner.delete_resource(resource);
882 }
883 stack
884 .resources
885 .retain(|r| new_logical_ids.contains(&r.logical_id));
886
887 let mut physical_ids: HashMap<String, String> = stack
889 .resources
890 .iter()
891 .map(|r| (r.logical_id.clone(), r.physical_id.clone()))
892 .collect();
893
894 for resource_def in new_resource_defs {
896 if !old_logical_ids.contains(&resource_def.logical_id) {
897 let resolved_def = template::resolve_resource_properties(
898 resource_def,
899 template_body,
900 parameters,
901 &physical_ids,
902 )
903 .map_err(|e| {
904 format!(
905 "Failed to resolve resource {}: {e}",
906 resource_def.logical_id
907 )
908 })?;
909
910 match provisioner.create_resource(&resolved_def) {
911 Ok(stack_resource) => {
912 physical_ids.insert(
913 stack_resource.logical_id.clone(),
914 stack_resource.physical_id.clone(),
915 );
916 stack.resources.push(stack_resource);
917 }
918 Err(e) => {
919 tracing::warn!(
920 "Failed to create resource {} during update: {e}",
921 resource_def.logical_id
922 );
923 return Err(format!(
924 "Failed to create resource {}: {e}",
925 resource_def.logical_id
926 ));
927 }
928 }
929 }
930 }
931
932 Ok(())
933}
934
935#[cfg(test)]
936mod tests {
937 use super::*;
938 use http::HeaderMap;
939 use parking_lot::RwLock;
940 use std::sync::Arc;
941
942 fn make_service() -> CloudFormationService {
943 let cf_state = Arc::new(RwLock::new(
944 fakecloud_core::multi_account::MultiAccountState::new(
945 "123456789012",
946 "us-east-1",
947 "http://localhost:4566",
948 ),
949 ));
950 let deps = CloudFormationDeps {
951 sqs: Arc::new(RwLock::new(
952 fakecloud_core::multi_account::MultiAccountState::new(
953 "123456789012",
954 "us-east-1",
955 "http://localhost:4566",
956 ),
957 )),
958 sns: Arc::new(RwLock::new(
959 fakecloud_core::multi_account::MultiAccountState::new(
960 "123456789012",
961 "us-east-1",
962 "http://localhost:4566",
963 ),
964 )),
965 ssm: Arc::new(RwLock::new(
966 fakecloud_core::multi_account::MultiAccountState::new(
967 "123456789012",
968 "us-east-1",
969 "http://localhost:4566",
970 ),
971 )),
972 iam: Arc::new(RwLock::new(
973 fakecloud_core::multi_account::MultiAccountState::new(
974 "123456789012",
975 "us-east-1",
976 "",
977 ),
978 )),
979 s3: Arc::new(RwLock::new(
980 fakecloud_core::multi_account::MultiAccountState::new(
981 "123456789012",
982 "us-east-1",
983 "",
984 ),
985 )),
986 eventbridge: Arc::new(RwLock::new(
987 fakecloud_core::multi_account::MultiAccountState::new(
988 "123456789012",
989 "us-east-1",
990 "",
991 ),
992 )),
993 dynamodb: Arc::new(RwLock::new(
994 fakecloud_core::multi_account::MultiAccountState::new(
995 "123456789012",
996 "us-east-1",
997 "",
998 ),
999 )),
1000 logs: Arc::new(RwLock::new(
1001 fakecloud_core::multi_account::MultiAccountState::new(
1002 "123456789012",
1003 "us-east-1",
1004 "",
1005 ),
1006 )),
1007 delivery: Arc::new(DeliveryBus::new()),
1008 };
1009 CloudFormationService::new(cf_state, deps)
1010 }
1011
1012 fn make_request(action: &str, params: HashMap<String, String>) -> AwsRequest {
1013 AwsRequest {
1014 service: "cloudformation".to_string(),
1015 action: action.to_string(),
1016 region: "us-east-1".to_string(),
1017 account_id: "123456789012".to_string(),
1018 request_id: "test-request-id".to_string(),
1019 headers: HeaderMap::new(),
1020 query_params: params,
1021 body: bytes::Bytes::new(),
1022 path_segments: vec![],
1023 raw_path: "/".to_string(),
1024 raw_query: String::new(),
1025 method: http::Method::POST,
1026 is_query_protocol: true,
1027 access_key_id: None,
1028 principal: None,
1029 }
1030 }
1031
1032 #[test]
1033 fn update_stack_sets_failed_status_on_resource_error() {
1034 let svc = make_service();
1035
1036 let mut create_params = HashMap::new();
1038 create_params.insert("StackName".to_string(), "test-stack".to_string());
1039 create_params.insert(
1040 "TemplateBody".to_string(),
1041 r#"{"Resources":{"MyQueue":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"q1"}}}}"#.to_string(),
1042 );
1043 let req = make_request("CreateStack", create_params);
1044 let result = svc.create_stack(&req);
1045 assert!(result.is_ok());
1046
1047 let mut update_params = HashMap::new();
1049 update_params.insert("StackName".to_string(), "test-stack".to_string());
1050 update_params.insert(
1051 "TemplateBody".to_string(),
1052 r#"{"Resources":{"MyQueue":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"q1"}},"BadSub":{"Type":"AWS::SNS::Subscription","Properties":{"TopicArn":"arn:aws:sns:us-east-1:123456789012:nope","Protocol":"sqs","Endpoint":"arn:aws:sqs:us-east-1:123456789012:q1"}}}}"#.to_string(),
1053 );
1054 let req = make_request("UpdateStack", update_params);
1055 let result = svc.update_stack(&req);
1056
1057 assert!(result.is_err());
1059
1060 let accounts = svc.state.read();
1062 let state = accounts.get("123456789012").unwrap();
1063 let stack = state.stacks.get("test-stack").unwrap();
1064 assert_eq!(stack.status, "UPDATE_FAILED");
1065 }
1066
1067 #[test]
1068 fn create_stack_resolves_ref_to_physical_id() {
1069 let svc = make_service();
1070
1071 let template = r#"{
1073 "Resources": {
1074 "MyTopic": {
1075 "Type": "AWS::SNS::Topic",
1076 "Properties": { "TopicName": "ref-test-topic" }
1077 },
1078 "MySub": {
1079 "Type": "AWS::SNS::Subscription",
1080 "Properties": {
1081 "TopicArn": { "Ref": "MyTopic" },
1082 "Protocol": "sqs",
1083 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:some-queue"
1084 }
1085 }
1086 }
1087 }"#;
1088
1089 let mut params = HashMap::new();
1090 params.insert("StackName".to_string(), "ref-stack".to_string());
1091 params.insert("TemplateBody".to_string(), template.to_string());
1092 let req = make_request("CreateStack", params);
1093 let result = svc.create_stack(&req);
1094 assert!(result.is_ok(), "CreateStack failed: {:?}", result.err());
1095
1096 let accounts = svc.state.read();
1098 let state = accounts.get("123456789012").unwrap();
1099 let stack = state.stacks.get("ref-stack").unwrap();
1100 assert_eq!(stack.resources.len(), 2);
1101 assert_eq!(stack.status, "CREATE_COMPLETE");
1102
1103 let sub = stack
1105 .resources
1106 .iter()
1107 .find(|r| r.logical_id == "MySub")
1108 .unwrap();
1109 assert!(
1110 sub.physical_id.contains("ref-test-topic"),
1111 "Subscription physical ID should reference the topic ARN, got: {}",
1112 sub.physical_id
1113 );
1114 }
1115
1116 #[test]
1119 fn create_stack_missing_name_errors() {
1120 let svc = make_service();
1121 let mut params = HashMap::new();
1122 params.insert("TemplateBody".to_string(), "{}".to_string());
1123 let req = make_request("CreateStack", params);
1124 assert!(svc.create_stack(&req).is_err());
1125 }
1126
1127 #[test]
1128 fn create_stack_missing_template_errors() {
1129 let svc = make_service();
1130 let mut params = HashMap::new();
1131 params.insert("StackName".to_string(), "s".to_string());
1132 let req = make_request("CreateStack", params);
1133 assert!(svc.create_stack(&req).is_err());
1134 }
1135
1136 #[test]
1137 fn create_stack_duplicate_errors() {
1138 let svc = make_service();
1139 let mut params = HashMap::new();
1140 params.insert("StackName".to_string(), "dup".to_string());
1141 params.insert(
1142 "TemplateBody".to_string(),
1143 r#"{"Resources":{"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"dq"}}}}"#
1144 .to_string(),
1145 );
1146 let req = make_request("CreateStack", params.clone());
1147 svc.create_stack(&req).unwrap();
1148 let req = make_request("CreateStack", params);
1149 assert!(svc.create_stack(&req).is_err());
1150 }
1151
1152 #[test]
1153 fn create_stack_invalid_template_errors() {
1154 let svc = make_service();
1155 let mut params = HashMap::new();
1156 params.insert("StackName".to_string(), "bad".to_string());
1157 params.insert("TemplateBody".to_string(), "not json".to_string());
1158 let req = make_request("CreateStack", params);
1159 assert!(svc.create_stack(&req).is_err());
1160 }
1161
1162 #[test]
1163 fn delete_stack_unknown_is_noop() {
1164 let svc = make_service();
1165 let mut params = HashMap::new();
1166 params.insert("StackName".to_string(), "ghost".to_string());
1167 let req = make_request("DeleteStack", params);
1168 assert!(svc.delete_stack(&req).is_ok());
1169 }
1170
1171 #[test]
1172 fn describe_stacks_nonexistent_errors() {
1173 let svc = make_service();
1174 let mut params = HashMap::new();
1175 params.insert("StackName".to_string(), "ghost".to_string());
1176 let req = make_request("DescribeStacks", params);
1177 assert!(svc.describe_stacks(&req).is_err());
1178 }
1179
1180 #[test]
1181 fn describe_stacks_empty_returns_all() {
1182 let svc = make_service();
1183 let req = make_request("DescribeStacks", HashMap::new());
1184 let resp = svc.describe_stacks(&req).unwrap();
1185 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
1186 assert!(b.contains("DescribeStacksResult"));
1187 }
1188
1189 #[test]
1190 fn list_stacks_empty_returns_ok() {
1191 let svc = make_service();
1192 let req = make_request("ListStacks", HashMap::new());
1193 let resp = svc.list_stacks(&req).unwrap();
1194 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
1195 assert!(b.contains("ListStacksResult"));
1196 }
1197
1198 #[test]
1199 fn list_stack_resources_missing_name_errors() {
1200 let svc = make_service();
1201 let req = make_request("ListStackResources", HashMap::new());
1202 assert!(svc.list_stack_resources(&req).is_err());
1203 }
1204
1205 #[test]
1206 fn list_stack_resources_unknown_stack_errors() {
1207 let svc = make_service();
1208 let mut params = HashMap::new();
1209 params.insert("StackName".to_string(), "ghost".to_string());
1210 let req = make_request("ListStackResources", params);
1211 assert!(svc.list_stack_resources(&req).is_err());
1212 }
1213
1214 #[test]
1215 fn describe_stack_resources_missing_name_errors() {
1216 let svc = make_service();
1217 let req = make_request("DescribeStackResources", HashMap::new());
1218 assert!(svc.describe_stack_resources(&req).is_err());
1219 }
1220
1221 #[test]
1222 fn get_template_missing_name_errors() {
1223 let svc = make_service();
1224 let req = make_request("GetTemplate", HashMap::new());
1225 assert!(svc.get_template(&req).is_err());
1226 }
1227
1228 #[test]
1229 fn get_template_unknown_stack_errors() {
1230 let svc = make_service();
1231 let mut params = HashMap::new();
1232 params.insert("StackName".to_string(), "ghost".to_string());
1233 let req = make_request("GetTemplate", params);
1234 assert!(svc.get_template(&req).is_err());
1235 }
1236
1237 #[test]
1238 fn update_stack_missing_name_errors() {
1239 let svc = make_service();
1240 let mut params = HashMap::new();
1241 params.insert("TemplateBody".to_string(), "{}".to_string());
1242 let req = make_request("UpdateStack", params);
1243 assert!(svc.update_stack(&req).is_err());
1244 }
1245
1246 #[test]
1247 fn update_stack_unknown_stack_errors() {
1248 let svc = make_service();
1249 let mut params = HashMap::new();
1250 params.insert("StackName".to_string(), "ghost".to_string());
1251 params.insert(
1252 "TemplateBody".to_string(),
1253 r#"{"Resources":{}}"#.to_string(),
1254 );
1255 let req = make_request("UpdateStack", params);
1256 assert!(svc.update_stack(&req).is_err());
1257 }
1258}