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 body_stream: parking_lot::Mutex::new(None),
1023 path_segments: vec![],
1024 raw_path: "/".to_string(),
1025 raw_query: String::new(),
1026 method: http::Method::POST,
1027 is_query_protocol: true,
1028 access_key_id: None,
1029 principal: None,
1030 }
1031 }
1032
1033 #[test]
1034 fn update_stack_sets_failed_status_on_resource_error() {
1035 let svc = make_service();
1036
1037 let mut create_params = HashMap::new();
1039 create_params.insert("StackName".to_string(), "test-stack".to_string());
1040 create_params.insert(
1041 "TemplateBody".to_string(),
1042 r#"{"Resources":{"MyQueue":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"q1"}}}}"#.to_string(),
1043 );
1044 let req = make_request("CreateStack", create_params);
1045 let result = svc.create_stack(&req);
1046 assert!(result.is_ok());
1047
1048 let mut update_params = HashMap::new();
1050 update_params.insert("StackName".to_string(), "test-stack".to_string());
1051 update_params.insert(
1052 "TemplateBody".to_string(),
1053 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(),
1054 );
1055 let req = make_request("UpdateStack", update_params);
1056 let result = svc.update_stack(&req);
1057
1058 assert!(result.is_err());
1060
1061 let accounts = svc.state.read();
1063 let state = accounts.get("123456789012").unwrap();
1064 let stack = state.stacks.get("test-stack").unwrap();
1065 assert_eq!(stack.status, "UPDATE_FAILED");
1066 }
1067
1068 #[test]
1069 fn create_stack_resolves_ref_to_physical_id() {
1070 let svc = make_service();
1071
1072 let template = r#"{
1074 "Resources": {
1075 "MyTopic": {
1076 "Type": "AWS::SNS::Topic",
1077 "Properties": { "TopicName": "ref-test-topic" }
1078 },
1079 "MySub": {
1080 "Type": "AWS::SNS::Subscription",
1081 "Properties": {
1082 "TopicArn": { "Ref": "MyTopic" },
1083 "Protocol": "sqs",
1084 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:some-queue"
1085 }
1086 }
1087 }
1088 }"#;
1089
1090 let mut params = HashMap::new();
1091 params.insert("StackName".to_string(), "ref-stack".to_string());
1092 params.insert("TemplateBody".to_string(), template.to_string());
1093 let req = make_request("CreateStack", params);
1094 let result = svc.create_stack(&req);
1095 assert!(result.is_ok(), "CreateStack failed: {:?}", result.err());
1096
1097 let accounts = svc.state.read();
1099 let state = accounts.get("123456789012").unwrap();
1100 let stack = state.stacks.get("ref-stack").unwrap();
1101 assert_eq!(stack.resources.len(), 2);
1102 assert_eq!(stack.status, "CREATE_COMPLETE");
1103
1104 let sub = stack
1106 .resources
1107 .iter()
1108 .find(|r| r.logical_id == "MySub")
1109 .unwrap();
1110 assert!(
1111 sub.physical_id.contains("ref-test-topic"),
1112 "Subscription physical ID should reference the topic ARN, got: {}",
1113 sub.physical_id
1114 );
1115 }
1116
1117 #[test]
1120 fn create_stack_missing_name_errors() {
1121 let svc = make_service();
1122 let mut params = HashMap::new();
1123 params.insert("TemplateBody".to_string(), "{}".to_string());
1124 let req = make_request("CreateStack", params);
1125 assert!(svc.create_stack(&req).is_err());
1126 }
1127
1128 #[test]
1129 fn create_stack_missing_template_errors() {
1130 let svc = make_service();
1131 let mut params = HashMap::new();
1132 params.insert("StackName".to_string(), "s".to_string());
1133 let req = make_request("CreateStack", params);
1134 assert!(svc.create_stack(&req).is_err());
1135 }
1136
1137 #[test]
1138 fn create_stack_duplicate_errors() {
1139 let svc = make_service();
1140 let mut params = HashMap::new();
1141 params.insert("StackName".to_string(), "dup".to_string());
1142 params.insert(
1143 "TemplateBody".to_string(),
1144 r#"{"Resources":{"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"dq"}}}}"#
1145 .to_string(),
1146 );
1147 let req = make_request("CreateStack", params.clone());
1148 svc.create_stack(&req).unwrap();
1149 let req = make_request("CreateStack", params);
1150 assert!(svc.create_stack(&req).is_err());
1151 }
1152
1153 #[test]
1154 fn create_stack_invalid_template_errors() {
1155 let svc = make_service();
1156 let mut params = HashMap::new();
1157 params.insert("StackName".to_string(), "bad".to_string());
1158 params.insert("TemplateBody".to_string(), "not json".to_string());
1159 let req = make_request("CreateStack", params);
1160 assert!(svc.create_stack(&req).is_err());
1161 }
1162
1163 #[test]
1164 fn delete_stack_unknown_is_noop() {
1165 let svc = make_service();
1166 let mut params = HashMap::new();
1167 params.insert("StackName".to_string(), "ghost".to_string());
1168 let req = make_request("DeleteStack", params);
1169 assert!(svc.delete_stack(&req).is_ok());
1170 }
1171
1172 #[test]
1173 fn describe_stacks_nonexistent_errors() {
1174 let svc = make_service();
1175 let mut params = HashMap::new();
1176 params.insert("StackName".to_string(), "ghost".to_string());
1177 let req = make_request("DescribeStacks", params);
1178 assert!(svc.describe_stacks(&req).is_err());
1179 }
1180
1181 #[test]
1182 fn describe_stacks_empty_returns_all() {
1183 let svc = make_service();
1184 let req = make_request("DescribeStacks", HashMap::new());
1185 let resp = svc.describe_stacks(&req).unwrap();
1186 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
1187 assert!(b.contains("DescribeStacksResult"));
1188 }
1189
1190 #[test]
1191 fn list_stacks_empty_returns_ok() {
1192 let svc = make_service();
1193 let req = make_request("ListStacks", HashMap::new());
1194 let resp = svc.list_stacks(&req).unwrap();
1195 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
1196 assert!(b.contains("ListStacksResult"));
1197 }
1198
1199 #[test]
1200 fn list_stack_resources_missing_name_errors() {
1201 let svc = make_service();
1202 let req = make_request("ListStackResources", HashMap::new());
1203 assert!(svc.list_stack_resources(&req).is_err());
1204 }
1205
1206 #[test]
1207 fn list_stack_resources_unknown_stack_errors() {
1208 let svc = make_service();
1209 let mut params = HashMap::new();
1210 params.insert("StackName".to_string(), "ghost".to_string());
1211 let req = make_request("ListStackResources", params);
1212 assert!(svc.list_stack_resources(&req).is_err());
1213 }
1214
1215 #[test]
1216 fn describe_stack_resources_missing_name_errors() {
1217 let svc = make_service();
1218 let req = make_request("DescribeStackResources", HashMap::new());
1219 assert!(svc.describe_stack_resources(&req).is_err());
1220 }
1221
1222 #[test]
1223 fn get_template_missing_name_errors() {
1224 let svc = make_service();
1225 let req = make_request("GetTemplate", HashMap::new());
1226 assert!(svc.get_template(&req).is_err());
1227 }
1228
1229 #[test]
1230 fn get_template_unknown_stack_errors() {
1231 let svc = make_service();
1232 let mut params = HashMap::new();
1233 params.insert("StackName".to_string(), "ghost".to_string());
1234 let req = make_request("GetTemplate", params);
1235 assert!(svc.get_template(&req).is_err());
1236 }
1237
1238 #[test]
1239 fn update_stack_missing_name_errors() {
1240 let svc = make_service();
1241 let mut params = HashMap::new();
1242 params.insert("TemplateBody".to_string(), "{}".to_string());
1243 let req = make_request("UpdateStack", params);
1244 assert!(svc.update_stack(&req).is_err());
1245 }
1246
1247 #[test]
1248 fn update_stack_unknown_stack_errors() {
1249 let svc = make_service();
1250 let mut params = HashMap::new();
1251 params.insert("StackName".to_string(), "ghost".to_string());
1252 params.insert(
1253 "TemplateBody".to_string(),
1254 r#"{"Resources":{}}"#.to_string(),
1255 );
1256 let req = make_request("UpdateStack", params);
1257 assert!(svc.update_stack(&req).is_err());
1258 }
1259}