1use async_trait::async_trait;
2use chrono::Utc;
3use http::StatusCode;
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7use fakecloud_core::delivery::DeliveryBus;
8use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
9use fakecloud_dynamodb::SharedDynamoDbState;
10use fakecloud_eventbridge::SharedEventBridgeState;
11use fakecloud_iam::SharedIamState;
12use fakecloud_logs::SharedLogsState;
13use fakecloud_persistence::SnapshotStore;
14use fakecloud_s3::SharedS3State;
15use fakecloud_sns::SharedSnsState;
16use fakecloud_sqs::SharedSqsState;
17use fakecloud_ssm::SharedSsmState;
18use tokio::sync::Mutex as AsyncMutex;
19
20use crate::resource_provisioner::ResourceProvisioner;
21use crate::state;
22use crate::state::{
23 CloudFormationSnapshot, CloudFormationState, SharedCloudFormationState, Stack, StackResource,
24 CLOUDFORMATION_SNAPSHOT_SCHEMA_VERSION,
25};
26use crate::template;
27use crate::xml_responses;
28
29fn well_known_attributes_for(resource_type: &str) -> &'static [&'static str] {
34 match resource_type {
35 "AWS::S3::Bucket" => &[
36 "Arn",
37 "DomainName",
38 "RegionalDomainName",
39 "DualStackDomainName",
40 "WebsiteURL",
41 ],
42 "AWS::Lambda::Function" => &["Arn", "FunctionUrl", "Version"],
43 "AWS::IAM::Role" => &["Arn", "RoleId"],
44 "AWS::SQS::Queue" => &["Arn", "QueueName", "QueueUrl"],
45 "AWS::SNS::Topic" => &["TopicArn", "TopicName"],
46 "AWS::DynamoDB::Table" => &["Arn", "StreamArn"],
47 "AWS::KMS::Key" => &["Arn", "KeyId"],
48 "AWS::SecretsManager::Secret" => &["Arn", "Id"],
49 "AWS::CloudFront::Distribution" => &["DomainName", "Id"],
50 _ => &[],
51 }
52}
53
54pub(crate) fn provision_stack_resources(
63 provisioner: &ResourceProvisioner,
64 resource_defs: &[template::ResourceDefinition],
65 template_body: &str,
66 parameters: &BTreeMap<String, String>,
67) -> Result<Vec<StackResource>, AwsServiceError> {
68 let mut resources = Vec::new();
69 let mut physical_ids: BTreeMap<String, String> = BTreeMap::new();
70 let mut attributes: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
71 let order = template::dependency_order(template_body, parameters, resource_defs);
76 let mut pending: Vec<&template::ResourceDefinition> =
77 order.iter().map(|&i| &resource_defs[i]).collect();
78 let max_passes = pending.len() + 1;
79
80 for _ in 0..max_passes {
81 if pending.is_empty() {
82 break;
83 }
84 let mut still_pending = Vec::new();
85 let mut made_progress = false;
86
87 for resource_def in pending {
88 let resolved_def = template::resolve_resource_properties_with_attrs(
89 resource_def,
90 template_body,
91 parameters,
92 &physical_ids,
93 &attributes,
94 )
95 .map_err(|e| {
96 AwsServiceError::aws_error(
100 StatusCode::BAD_REQUEST,
101 "InsufficientCapabilitiesException",
102 e,
103 )
104 })?;
105
106 match provisioner.create_resource(&resolved_def) {
107 Ok(stack_resource) => {
108 physical_ids.insert(
109 stack_resource.logical_id.clone(),
110 stack_resource.physical_id.clone(),
111 );
112 let mut attr_map = stack_resource.attributes.clone();
117 for attr in well_known_attributes_for(&stack_resource.resource_type) {
118 if attr_map.contains_key(*attr) {
119 continue;
120 }
121 if let Some(v) = provisioner.get_att(&stack_resource, attr) {
122 attr_map.insert((*attr).to_string(), v);
123 }
124 }
125 attributes.insert(stack_resource.logical_id.clone(), attr_map);
126 resources.push(stack_resource);
127 made_progress = true;
128 }
129 Err(_) => still_pending.push(resource_def),
130 }
131 }
132
133 pending = still_pending;
134 if !made_progress && !pending.is_empty() {
135 let resource_def = pending[0];
138 let resolved_def = template::resolve_resource_properties_with_attrs(
139 resource_def,
140 template_body,
141 parameters,
142 &physical_ids,
143 &attributes,
144 )
145 .unwrap_or_else(|_| resource_def.clone());
146 let err = provisioner.create_resource(&resolved_def).unwrap_err();
147 for r in &resources {
148 let _ = provisioner.delete_resource(r);
149 }
150 return Err(AwsServiceError::aws_error(
151 StatusCode::BAD_REQUEST,
152 "ValidationError",
153 format!(
154 "Failed to create resource {}: {err}",
155 resource_def.logical_id
156 ),
157 ));
158 }
159 }
160
161 Ok(resources)
162}
163
164pub struct CloudFormationDeps {
166 pub sqs: SharedSqsState,
167 pub sns: SharedSnsState,
168 pub ssm: SharedSsmState,
169 pub iam: SharedIamState,
170 pub s3: SharedS3State,
171 pub eventbridge: SharedEventBridgeState,
172 pub dynamodb: SharedDynamoDbState,
173 pub logs: SharedLogsState,
174 pub lambda: fakecloud_lambda::SharedLambdaState,
175 pub secretsmanager: fakecloud_secretsmanager::SharedSecretsManagerState,
176 pub kinesis: fakecloud_kinesis::SharedKinesisState,
177 pub kms: fakecloud_kms::SharedKmsState,
178 pub ecr: fakecloud_ecr::SharedEcrState,
179 pub cloudwatch: fakecloud_cloudwatch::SharedCloudWatchState,
180 pub elbv2: fakecloud_elbv2::SharedElbv2State,
181 pub organizations: fakecloud_organizations::SharedOrganizationsState,
182 pub cognito: fakecloud_cognito::SharedCognitoState,
183 pub rds: fakecloud_rds::SharedRdsState,
184 pub ecs: fakecloud_ecs::SharedEcsState,
185 pub acm: fakecloud_acm::SharedAcmState,
186 pub elasticache: fakecloud_elasticache::SharedElastiCacheState,
187 pub route53: fakecloud_route53::SharedRoute53State,
188 pub cloudfront: fakecloud_cloudfront::SharedCloudFrontState,
189 pub stepfunctions: fakecloud_stepfunctions::SharedStepFunctionsState,
190 pub wafv2: fakecloud_wafv2::SharedWafv2State,
191 pub apigateway: fakecloud_apigateway::SharedApiGatewayState,
192 pub apigatewayv2: fakecloud_apigatewayv2::SharedApiGatewayV2State,
193 pub ses: fakecloud_ses::SharedSesState,
194 pub application_autoscaling:
195 fakecloud_application_autoscaling::SharedApplicationAutoScalingState,
196 pub athena: fakecloud_athena::SharedAthenaState,
197 pub firehose: fakecloud_firehose::SharedFirehoseState,
198 pub glue: fakecloud_glue::SharedGlueState,
199 pub delivery: Arc<DeliveryBus>,
200 pub lambda_runtime: Option<Arc<fakecloud_lambda::runtime::ContainerRuntime>>,
207}
208
209pub struct CloudFormationService {
210 pub(crate) state: SharedCloudFormationState,
211 pub(crate) deps: CloudFormationDeps,
212 snapshot_store: Option<Arc<dyn SnapshotStore>>,
213 snapshot_lock: Arc<AsyncMutex<()>>,
214}
215
216struct CreateStackContext {
220 state: SharedCloudFormationState,
221 delivery: Arc<DeliveryBus>,
222 snapshot_store: Option<Arc<dyn SnapshotStore>>,
223 snapshot_lock: Arc<AsyncMutex<()>>,
224 provisioner: ResourceProvisioner,
225 account_id: String,
226 stack_name: String,
227 stack_id: String,
228 template_body: String,
229 parameters: BTreeMap<String, String>,
230 notification_arns: Vec<String>,
231 imported_names: Vec<String>,
232 resource_defs: Vec<template::ResourceDefinition>,
233}
234
235impl CloudFormationService {
236 pub fn new(state: SharedCloudFormationState, deps: CloudFormationDeps) -> Self {
237 Self {
238 state,
239 deps,
240 snapshot_store: None,
241 snapshot_lock: Arc::new(AsyncMutex::new(())),
242 }
243 }
244
245 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
246 self.snapshot_store = Some(store);
247 self
248 }
249
250 async fn save_snapshot(&self) {
251 let Some(store) = self.snapshot_store.clone() else {
252 return;
253 };
254 let _guard = self.snapshot_lock.lock().await;
255 let snapshot = CloudFormationSnapshot {
256 schema_version: CLOUDFORMATION_SNAPSHOT_SCHEMA_VERSION,
257 state: None,
258 accounts: Some(self.state.read().clone()),
259 };
260 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
261 let bytes = serde_json::to_vec(&snapshot)
262 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
263 store.save(&bytes)
264 })
265 .await;
266 match join {
267 Ok(Ok(())) => {}
268 Ok(Err(err)) => tracing::error!(%err, "failed to write cloudformation snapshot"),
269 Err(err) => tracing::error!(%err, "cloudformation snapshot task panicked"),
270 }
271 }
272
273 pub(crate) fn provisioner(
274 &self,
275 stack_id: &str,
276 account_id: &str,
277 region: &str,
278 ) -> ResourceProvisioner {
279 ResourceProvisioner {
280 sqs_state: self.deps.sqs.clone(),
281 sns_state: self.deps.sns.clone(),
282 ssm_state: self.deps.ssm.clone(),
283 iam_state: self.deps.iam.clone(),
284 s3_state: self.deps.s3.clone(),
285 eventbridge_state: self.deps.eventbridge.clone(),
286 dynamodb_state: self.deps.dynamodb.clone(),
287 logs_state: self.deps.logs.clone(),
288 lambda_state: self.deps.lambda.clone(),
289 secretsmanager_state: self.deps.secretsmanager.clone(),
290 kinesis_state: self.deps.kinesis.clone(),
291 kms_state: self.deps.kms.clone(),
292 ecr_state: self.deps.ecr.clone(),
293 cloudwatch_state: self.deps.cloudwatch.clone(),
294 elbv2_state: self.deps.elbv2.clone(),
295 organizations_state: self.deps.organizations.clone(),
296 cognito_state: self.deps.cognito.clone(),
297 rds_state: self.deps.rds.clone(),
298 ecs_state: self.deps.ecs.clone(),
299 acm_state: self.deps.acm.clone(),
300 elasticache_state: self.deps.elasticache.clone(),
301 route53_state: self.deps.route53.clone(),
302 cloudfront_state: self.deps.cloudfront.clone(),
303 stepfunctions_state: self.deps.stepfunctions.clone(),
304 wafv2_state: self.deps.wafv2.clone(),
305 apigateway_state: self.deps.apigateway.clone(),
306 apigatewayv2_state: self.deps.apigatewayv2.clone(),
307 ses_state: self.deps.ses.clone(),
308 app_autoscaling_state: self.deps.application_autoscaling.clone(),
309 athena_state: self.deps.athena.clone(),
310 firehose_state: self.deps.firehose.clone(),
311 glue_state: self.deps.glue.clone(),
312 cloudformation_state: self.state.clone(),
313 delivery: self.deps.delivery.clone(),
314 lambda_runtime: self.deps.lambda_runtime.clone(),
315 account_id: account_id.to_string(),
316 region: region.to_string(),
317 stack_id: stack_id.to_string(),
318 }
319 }
320
321 fn get_param(req: &AwsRequest, key: &str) -> Option<String> {
322 if let Some(v) = req.query_params.get(key) {
324 return Some(v.clone());
325 }
326 let body_params = fakecloud_core::protocol::parse_query_body(&req.body);
328 body_params.get(key).cloned()
329 }
330
331 pub(crate) fn get_all_params(req: &AwsRequest) -> BTreeMap<String, String> {
332 let mut params: BTreeMap<String, String> = req.query_params.clone().into_iter().collect();
333 let body_params = fakecloud_core::protocol::parse_query_body(&req.body);
334 for (k, v) in body_params {
335 params.entry(k).or_insert(v);
336 }
337 params
338 }
339
340 pub(crate) fn extract_tags(params: &BTreeMap<String, String>) -> BTreeMap<String, String> {
341 let mut tags = BTreeMap::new();
342 for i in 1.. {
343 let key_param = format!("Tags.member.{i}.Key");
344 let value_param = format!("Tags.member.{i}.Value");
345 match (params.get(&key_param), params.get(&value_param)) {
346 (Some(k), Some(v)) => {
347 tags.insert(k.clone(), v.clone());
348 }
349 _ => break,
350 }
351 }
352 tags
353 }
354
355 pub(crate) fn extract_parameters(
356 params: &BTreeMap<String, String>,
357 ) -> BTreeMap<String, String> {
358 let mut result = BTreeMap::new();
359 for i in 1.. {
360 let key_param = format!("Parameters.member.{i}.ParameterKey");
361 let value_param = format!("Parameters.member.{i}.ParameterValue");
362 match (params.get(&key_param), params.get(&value_param)) {
363 (Some(k), Some(v)) => {
364 result.insert(k.clone(), v.clone());
365 }
366 _ => break,
367 }
368 }
369 result
370 }
371
372 pub(crate) fn extract_notification_arns(params: &BTreeMap<String, String>) -> Vec<String> {
373 let mut arns = Vec::new();
374 for i in 1.. {
375 let key = format!("NotificationARNs.member.{i}");
376 match params.get(&key) {
377 Some(arn) => arns.push(arn.clone()),
378 None => break,
379 }
380 }
381 arns
382 }
383
384 fn send_stack_notification(
385 delivery: &DeliveryBus,
386 notification_arns: &[String],
387 stack_name: &str,
388 stack_id: &str,
389 status: &str,
390 ) {
391 if notification_arns.is_empty() {
392 return;
393 }
394 let message = format!(
395 "StackId='{}'\nTimestamp='{}'\nEventId='{}'\nLogicalResourceId='{}'\nResourceStatus='{}'\nResourceType='AWS::CloudFormation::Stack'\nStackName='{}'",
396 stack_id,
397 chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ"),
398 uuid::Uuid::new_v4(),
399 stack_name,
400 status,
401 stack_name,
402 );
403 for arn in notification_arns {
404 delivery.publish_to_sns(arn, &message, Some("AWS CloudFormation Notification"));
405 }
406 }
407
408 fn collect_account_imports(
413 state: &SharedCloudFormationState,
414 account_id: &str,
415 skip_stack: Option<&str>,
416 ) -> BTreeMap<String, String> {
417 let mut imports = BTreeMap::new();
418 let accounts = state.read();
419 let Some(state) = accounts.get(account_id) else {
420 return imports;
421 };
422 for (name, export) in &state.exports {
423 if matches!(skip_stack, Some(skip) if skip == export.exporting_stack_name) {
424 continue;
425 }
426 imports.insert(name.clone(), export.value.clone());
427 }
428 imports
429 }
430
431 fn validate_import_values(
436 state: &SharedCloudFormationState,
437 account_id: &str,
438 stack_name: &str,
439 template_body: &str,
440 parameters: &BTreeMap<String, String>,
441 ) -> Result<Vec<String>, AwsServiceError> {
442 let value: serde_json::Value = if template_body.trim_start().starts_with('{') {
443 match serde_json::from_str(template_body) {
444 Ok(v) => v,
445 Err(_) => return Ok(Vec::new()),
446 }
447 } else {
448 match serde_yaml::from_str(template_body) {
449 Ok(v) => v,
450 Err(_) => return Ok(Vec::new()),
451 }
452 };
453 let names = template::collect_import_value_names(&value, parameters);
454 let known = Self::collect_account_imports(state, account_id, Some(stack_name));
455 for n in &names {
456 if !known.contains_key(n) {
457 return Err(AwsServiceError::aws_error(
462 StatusCode::BAD_REQUEST,
463 "InsufficientCapabilitiesException",
464 format!("No export named {n} found."),
465 ));
466 }
467 }
468 Ok(names)
469 }
470
471 pub(crate) fn sync_exports_imports(
475 state: &mut CloudFormationState,
476 stack_id: &str,
477 stack_name: &str,
478 outputs: &[state::StackOutput],
479 imported_names: &[String],
480 ) {
481 let stale_exports: Vec<String> = state
483 .exports
484 .iter()
485 .filter(|(_, e)| e.exporting_stack_name == stack_name)
486 .map(|(k, _)| k.clone())
487 .collect();
488 for k in stale_exports {
489 state.exports.remove(&k);
490 }
491 for entries in state.imports.values_mut() {
493 entries.retain(|s| s != stack_name);
494 }
495 state.imports.retain(|_, v| !v.is_empty());
496
497 for o in outputs {
499 if let Some(export) = &o.export_name {
500 state.exports.insert(
501 export.clone(),
502 state::StackExport {
503 value: o.value.clone(),
504 exporting_stack_id: stack_id.to_string(),
505 exporting_stack_name: stack_name.to_string(),
506 },
507 );
508 }
509 }
510 for name in imported_names {
512 let entry = state.imports.entry(name.clone()).or_default();
513 if !entry.iter().any(|s| s == stack_name) {
514 entry.push(stack_name.to_string());
515 }
516 }
517 }
518
519 pub(crate) fn resolve_template_outputs(
524 template_body: &str,
525 parameters: &BTreeMap<String, String>,
526 resources: &[StackResource],
527 state: &SharedCloudFormationState,
528 ) -> Vec<state::StackOutput> {
529 let value: serde_json::Value = if template_body.trim_start().starts_with('{') {
530 match serde_json::from_str(template_body) {
531 Ok(v) => v,
532 Err(_) => return Vec::new(),
533 }
534 } else {
535 match serde_yaml::from_str(template_body) {
536 Ok(v) => v,
537 Err(_) => return Vec::new(),
538 }
539 };
540
541 let resources_obj = match value.get("Resources").and_then(|v| v.as_object()) {
542 Some(o) => o.clone(),
543 None => return Vec::new(),
544 };
545
546 let mut physical_ids: BTreeMap<String, String> = BTreeMap::new();
547 let mut attributes: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
548 for r in resources {
549 physical_ids.insert(r.logical_id.clone(), r.physical_id.clone());
550 attributes.insert(r.logical_id.clone(), r.attributes.clone());
551 }
552
553 let imports = {
554 let accounts = state.read();
555 let mut out = BTreeMap::new();
556 for (_account, st) in accounts.iter() {
559 for (name, export) in &st.exports {
560 out.insert(name.clone(), export.value.clone());
561 }
562 }
563 out
564 };
565
566 let parsed = match template::parse_outputs(
567 &value,
568 parameters,
569 &resources_obj,
570 &physical_ids,
571 &attributes,
572 &imports,
573 ) {
574 Ok(o) => o,
575 Err(_) => return Vec::new(),
576 };
577
578 parsed
579 .into_iter()
580 .map(|o| state::StackOutput {
581 key: o.logical_id,
582 value: o.value,
583 description: o.description,
584 export_name: o.export_name,
585 })
586 .collect()
587 }
588
589 fn ensure_export_uniqueness(
592 state: &SharedCloudFormationState,
593 account_id: &str,
594 stack_name: &str,
595 outputs: &[state::StackOutput],
596 ) -> Result<(), AwsServiceError> {
597 let existing = Self::collect_account_imports(state, account_id, Some(stack_name));
598 for o in outputs {
599 if let Some(export) = &o.export_name {
600 if existing.contains_key(export) {
601 return Err(AwsServiceError::aws_error(
605 StatusCode::BAD_REQUEST,
606 "AlreadyExistsException",
607 format!("Export with name {export} is already exported by another stack"),
608 ));
609 }
610 }
611 }
612 Ok(())
613 }
614
615 async fn create_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
616 let params = Self::get_all_params(req);
617
618 let stack_name = params.get("StackName").ok_or_else(|| {
621 AwsServiceError::aws_error(
622 StatusCode::BAD_REQUEST,
623 "ValidationError",
624 "StackName is required",
625 )
626 })?;
627
628 let empty = String::new();
632 let template_body = params.get("TemplateBody").unwrap_or(&empty);
633
634 {
636 let accounts = self.state.read();
637 let empty = CloudFormationState::new(&req.account_id, &req.region);
638 let state = accounts.get(&req.account_id).unwrap_or(&empty);
639 if let Some(existing) = state.stacks.get(stack_name.as_str()) {
640 if existing.status != "DELETE_COMPLETE" {
641 return Err(AwsServiceError::aws_error(
642 StatusCode::BAD_REQUEST,
643 "AlreadyExistsException",
644 format!("Stack [{stack_name}] already exists"),
645 ));
646 }
647 }
648 }
649
650 let tags = Self::extract_tags(¶ms);
651 let mut parameters = Self::extract_parameters(¶ms);
652 let notification_arns = Self::extract_notification_arns(¶ms);
653
654 let stack_id = format!(
657 "arn:aws:cloudformation:{}:{}:stack/{}/{}",
658 req.region,
659 req.account_id,
660 stack_name,
661 uuid::Uuid::new_v4()
662 );
663 parameters
664 .entry("AWS::Region".to_string())
665 .or_insert_with(|| req.region.clone());
666 parameters
667 .entry("AWS::AccountId".to_string())
668 .or_insert_with(|| req.account_id.clone());
669 parameters
670 .entry("AWS::StackId".to_string())
671 .or_insert_with(|| stack_id.clone());
672 parameters
673 .entry("AWS::StackName".to_string())
674 .or_insert_with(|| stack_name.clone());
675 parameters
676 .entry("AWS::Partition".to_string())
677 .or_insert_with(|| template::partition_for_region(&req.region).to_string());
678 parameters
679 .entry("AWS::URLSuffix".to_string())
680 .or_insert_with(|| template::url_suffix_for_region(&req.region).to_string());
681 parameters.insert(
685 "AWS::NotificationARNs".to_string(),
686 serde_json::to_string(¬ification_arns).unwrap_or_else(|_| "[]".to_string()),
687 );
688
689 let parsed = template::parse_template(template_body, ¶meters).unwrap_or_else(|_| {
694 template::ParsedTemplate {
695 description: None,
696 resources: Vec::new(),
697 outputs: Vec::new(),
698 }
699 });
700
701 let imported_names = Self::validate_import_values(
705 &self.state,
706 &req.account_id,
707 stack_name,
708 template_body,
709 ¶meters,
710 )?;
711
712 {
719 let mut accounts = self.state.write();
720 let state = accounts.get_or_create(&req.account_id);
721 state.stacks.insert(
722 stack_name.clone(),
723 Stack {
724 name: stack_name.clone(),
725 stack_id: stack_id.clone(),
726 template: template_body.clone(),
727 status: "CREATE_IN_PROGRESS".to_string(),
728 resources: Vec::new(),
729 parameters: parameters.clone(),
730 tags: tags.clone(),
731 created_at: Utc::now(),
732 updated_at: None,
733 description: parsed.description.clone(),
734 notification_arns: notification_arns.clone(),
735 outputs: Vec::new(),
736 },
737 );
738 record_stack_status_event(
739 state,
740 &stack_id,
741 stack_name,
742 "AWS::CloudFormation::Stack",
743 "CREATE_IN_PROGRESS",
744 );
745 }
746
747 let ctx = CreateStackContext {
748 state: self.state.clone(),
749 delivery: self.deps.delivery.clone(),
750 snapshot_store: self.snapshot_store.clone(),
751 snapshot_lock: self.snapshot_lock.clone(),
752 provisioner: self.provisioner(&stack_id, &req.account_id, &req.region),
753 account_id: req.account_id.clone(),
754 stack_name: stack_name.clone(),
755 stack_id: stack_id.clone(),
756 template_body: template_body.clone(),
757 parameters,
758 notification_arns,
759 imported_names,
760 resource_defs: parsed.resources,
761 };
762
763 let has_custom_resource = ctx.resource_defs.iter().any(|r| {
779 r.resource_type.starts_with("Custom::")
780 || r.resource_type == "AWS::CloudFormation::CustomResource"
781 });
782 let multi_thread = matches!(
783 tokio::runtime::Handle::try_current().map(|h| h.runtime_flavor()),
784 Ok(tokio::runtime::RuntimeFlavor::MultiThread)
785 );
786 if has_custom_resource && multi_thread {
787 Self::send_stack_notification(
792 &self.deps.delivery,
793 &ctx.notification_arns,
794 stack_name,
795 &stack_id,
796 "CREATE_IN_PROGRESS",
797 );
798 tokio::spawn(async move {
799 Self::finish_create_stack(ctx).await;
800 });
801 } else {
802 Self::finish_create_stack(ctx).await;
803 }
804
805 Ok(AwsResponse::xml(
806 StatusCode::OK,
807 xml_responses::create_stack_response(&stack_id, &req.request_id),
808 ))
809 }
810
811 async fn finish_create_stack(ctx: CreateStackContext) {
817 let CreateStackContext {
818 state,
819 delivery,
820 snapshot_store,
821 snapshot_lock,
822 provisioner,
823 account_id,
824 stack_name,
825 stack_id,
826 template_body,
827 parameters,
828 notification_arns,
829 imported_names,
830 resource_defs,
831 } = ctx;
832
833 let provision_result = {
837 let template_body = template_body.clone();
838 let parameters = parameters.clone();
839 tokio::task::spawn_blocking(move || {
840 provision_stack_resources(&provisioner, &resource_defs, &template_body, ¶meters)
841 })
842 .await
843 };
844
845 let provisioned = match provision_result {
848 Ok(Ok(resources)) => Ok(resources),
849 Ok(Err(err)) => Err(err.message()),
850 Err(join_err) => Err(format!("provisioning task failed: {join_err}")),
851 };
852
853 let resources = match provisioned {
854 Ok(resources) => resources,
855 Err(reason) => {
856 Self::mark_create_failed(
857 &state,
858 &delivery,
859 &account_id,
860 &stack_name,
861 &stack_id,
862 ¬ification_arns,
863 &reason,
864 );
865 save_snapshot_static(state.clone(), snapshot_store, snapshot_lock).await;
866 return;
867 }
868 };
869
870 let outputs =
871 Self::resolve_template_outputs(&template_body, ¶meters, &resources, &state);
872
873 if let Err(err) = Self::ensure_export_uniqueness(&state, &account_id, &stack_name, &outputs)
876 {
877 Self::mark_create_failed(
878 &state,
879 &delivery,
880 &account_id,
881 &stack_name,
882 &stack_id,
883 ¬ification_arns,
884 &err.message(),
885 );
886 save_snapshot_static(state.clone(), snapshot_store, snapshot_lock).await;
887 return;
888 }
889
890 {
891 let mut accounts = state.write();
892 let st = accounts.get_or_create(&account_id);
893 if let Some(stack) = st.stacks.get_mut(&stack_name) {
894 stack.status = "CREATE_COMPLETE".to_string();
895 stack.resources = resources.clone();
896 stack.outputs = outputs.clone();
897 }
898 Self::sync_exports_imports(st, &stack_id, &stack_name, &outputs, &imported_names);
899
900 let changes: Vec<ResourceChange> = resources
901 .iter()
902 .map(|r| ResourceChange {
903 action: ResourceChangeAction::Create,
904 logical_id: r.logical_id.clone(),
905 physical_id: r.physical_id.clone(),
906 resource_type: r.resource_type.clone(),
907 })
908 .collect();
909 record_stack_events(st, &stack_id, &stack_name, &changes);
910 record_stack_status_event(
911 st,
912 &stack_id,
913 &stack_name,
914 "AWS::CloudFormation::Stack",
915 "CREATE_COMPLETE",
916 );
917 }
918
919 Self::send_stack_notification(
920 &delivery,
921 ¬ification_arns,
922 &stack_name,
923 &stack_id,
924 "CREATE_COMPLETE",
925 );
926
927 save_snapshot_static(state, snapshot_store, snapshot_lock).await;
928 }
929
930 fn mark_create_failed(
934 state: &SharedCloudFormationState,
935 delivery: &DeliveryBus,
936 account_id: &str,
937 stack_name: &str,
938 stack_id: &str,
939 notification_arns: &[String],
940 reason: &str,
941 ) {
942 tracing::warn!(%stack_name, %reason, "CreateStack provisioning failed");
943 {
944 let mut accounts = state.write();
945 let st = accounts.get_or_create(account_id);
946 if let Some(stack) = st.stacks.get_mut(stack_name) {
947 stack.status = "CREATE_FAILED".to_string();
948 }
949 record_stack_status_event(
950 st,
951 stack_id,
952 stack_name,
953 "AWS::CloudFormation::Stack",
954 "CREATE_FAILED",
955 );
956 }
957 Self::send_stack_notification(
958 delivery,
959 notification_arns,
960 stack_name,
961 stack_id,
962 "CREATE_FAILED",
963 );
964 }
965
966 fn delete_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
967 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
968 AwsServiceError::aws_error(
969 StatusCode::BAD_REQUEST,
970 "ValidationError",
971 "StackName is required",
972 )
973 })?;
974
975 let mut accounts = self.state.write();
976 let state = accounts.get_or_create(&req.account_id);
977
978 let stack = state.stacks.values_mut().find(|s| {
980 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
981 });
982
983 if let Some(stack) = stack {
984 let stack_id = stack.stack_id.clone();
985 let stack_name_for_notif = stack.name.clone();
986 let notification_arns = stack.notification_arns.clone();
987 let resources: Vec<_> = stack.resources.clone();
988
989 let owned_exports: Vec<String> = state
992 .exports
993 .iter()
994 .filter(|(_, e)| e.exporting_stack_name == stack_name_for_notif)
995 .map(|(k, _)| k.clone())
996 .collect();
997 for export in &owned_exports {
998 if let Some(consumers) = state.imports.get(export) {
999 let consumers: Vec<&String> = consumers
1000 .iter()
1001 .filter(|c| **c != stack_name_for_notif)
1002 .collect();
1003 if !consumers.is_empty() {
1004 let names: Vec<&str> = consumers.iter().map(|s| s.as_str()).collect();
1005 return Err(AwsServiceError::aws_error(
1012 StatusCode::BAD_REQUEST,
1013 "TokenAlreadyExistsException",
1014 format!(
1015 "Export {export} cannot be deleted as it is in use by {}",
1016 names.join(", ")
1017 ),
1018 ));
1019 }
1020 }
1021 }
1022
1023 drop(accounts);
1026 let provisioner = self.provisioner(&stack_id, &req.account_id, &req.region);
1027
1028 for resource in resources.iter().rev() {
1030 let _ = provisioner.delete_resource(resource);
1031 }
1032
1033 let mut accounts = self.state.write();
1035 let state = accounts.get_or_create(&req.account_id);
1036 if let Some(stack) = state.stacks.values_mut().find(|s| s.stack_id == stack_id) {
1037 stack.status = "DELETE_COMPLETE".to_string();
1038 stack.resources.clear();
1039 stack.outputs.clear();
1040 }
1041 let stale_exports: Vec<String> = state
1043 .exports
1044 .iter()
1045 .filter(|(_, e)| e.exporting_stack_name == stack_name_for_notif)
1046 .map(|(k, _)| k.clone())
1047 .collect();
1048 for k in stale_exports {
1049 state.exports.remove(&k);
1050 }
1051 for entries in state.imports.values_mut() {
1052 entries.retain(|s| s != &stack_name_for_notif);
1053 }
1054 state.imports.retain(|_, v| !v.is_empty());
1055 drop(accounts);
1056
1057 Self::send_stack_notification(
1058 &self.deps.delivery,
1059 ¬ification_arns,
1060 &stack_name_for_notif,
1061 &stack_id,
1062 "DELETE_COMPLETE",
1063 );
1064 }
1065
1066 Ok(AwsResponse::xml(
1067 StatusCode::OK,
1068 xml_responses::delete_stack_response(&req.request_id),
1069 ))
1070 }
1071
1072 fn describe_stacks(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1073 let stack_name = Self::get_param(req, "StackName");
1074
1075 let accounts = self.state.read();
1076 let empty = CloudFormationState::new(&req.account_id, &req.region);
1077 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1078 let stacks: Vec<Stack> = if let Some(ref name) = stack_name {
1079 state
1080 .stacks
1081 .values()
1082 .filter(|s| {
1083 (s.name == *name || s.stack_id == *name) && s.status != "DELETE_COMPLETE"
1084 })
1085 .cloned()
1086 .collect()
1087 } else {
1088 state
1089 .stacks
1090 .values()
1091 .filter(|s| s.status != "DELETE_COMPLETE")
1092 .cloned()
1093 .collect()
1094 };
1095
1096 if let Some(ref name) = stack_name {
1107 if stacks.is_empty() {
1108 return Err(AwsServiceError::aws_error(
1109 StatusCode::BAD_REQUEST,
1110 "ValidationError",
1111 format!("Stack with id {name} does not exist"),
1112 ));
1113 }
1114 }
1115
1116 Ok(AwsResponse::xml(
1117 StatusCode::OK,
1118 xml_responses::describe_stacks_response(&stacks, &req.request_id),
1119 ))
1120 }
1121
1122 fn list_stacks(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1123 let accounts = self.state.read();
1124 let empty = CloudFormationState::new(&req.account_id, &req.region);
1125 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1126 let stacks: Vec<Stack> = state.stacks.values().cloned().collect();
1127
1128 Ok(AwsResponse::xml(
1129 StatusCode::OK,
1130 xml_responses::list_stacks_response(&stacks, &req.request_id),
1131 ))
1132 }
1133
1134 fn list_stack_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1135 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
1141 AwsServiceError::aws_error(
1142 StatusCode::BAD_REQUEST,
1143 "ValidationError",
1144 "StackName is required",
1145 )
1146 })?;
1147
1148 let accounts = self.state.read();
1149 let empty = CloudFormationState::new(&req.account_id, &req.region);
1150 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1151 let resources = state
1152 .stacks
1153 .values()
1154 .find(|s| {
1155 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
1156 })
1157 .map(|s| s.resources.clone())
1158 .unwrap_or_default();
1159
1160 Ok(AwsResponse::xml(
1161 StatusCode::OK,
1162 xml_responses::list_stack_resources_response(&resources, &req.request_id),
1163 ))
1164 }
1165
1166 fn describe_stack_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1167 let stack_name = Self::get_param(req, "StackName").unwrap_or_default();
1170
1171 let accounts = self.state.read();
1172 let empty = CloudFormationState::new(&req.account_id, &req.region);
1173 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1174 let (resources, resolved_name) = state
1175 .stacks
1176 .values()
1177 .find(|s| {
1178 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
1179 })
1180 .map(|s| (s.resources.clone(), s.name.clone()))
1181 .unwrap_or_else(|| (Vec::new(), stack_name.clone()));
1182
1183 Ok(AwsResponse::xml(
1184 StatusCode::OK,
1185 xml_responses::describe_stack_resources_response(
1186 &resources,
1187 &resolved_name,
1188 &req.request_id,
1189 ),
1190 ))
1191 }
1192
1193 fn update_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1194 let mut input = UpdateStackInput::from_params(req)?;
1195
1196 let found_stack_id = {
1198 let accounts = self.state.read();
1199 let empty = CloudFormationState::new(&req.account_id, &req.region);
1200 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1201 state
1202 .stacks
1203 .values()
1204 .find(|s| {
1205 (s.name == input.stack_name || s.stack_id == input.stack_name)
1206 && s.status != "DELETE_COMPLETE"
1207 })
1208 .map(|s| s.stack_id.clone())
1209 .unwrap_or_default()
1210 };
1211
1212 input
1216 .parameters
1217 .entry("AWS::Region".to_string())
1218 .or_insert_with(|| req.region.clone());
1219 input
1220 .parameters
1221 .entry("AWS::AccountId".to_string())
1222 .or_insert_with(|| req.account_id.clone());
1223 input
1224 .parameters
1225 .entry("AWS::StackId".to_string())
1226 .or_insert_with(|| found_stack_id.clone());
1227 input
1228 .parameters
1229 .entry("AWS::StackName".to_string())
1230 .or_insert_with(|| input.stack_name.clone());
1231 input
1232 .parameters
1233 .entry("AWS::Partition".to_string())
1234 .or_insert_with(|| template::partition_for_region(&req.region).to_string());
1235 input
1236 .parameters
1237 .entry("AWS::URLSuffix".to_string())
1238 .or_insert_with(|| template::url_suffix_for_region(&req.region).to_string());
1239 if !input.notification_arns.is_empty() {
1244 input.parameters.insert(
1245 "AWS::NotificationARNs".to_string(),
1246 serde_json::to_string(&input.notification_arns)
1247 .unwrap_or_else(|_| "[]".to_string()),
1248 );
1249 } else {
1250 let existing: Vec<String> = {
1253 let accounts = self.state.read();
1254 accounts
1255 .get(&req.account_id)
1256 .and_then(|s| {
1257 s.stacks
1258 .values()
1259 .find(|st| st.stack_id == found_stack_id)
1260 .map(|st| st.notification_arns.clone())
1261 })
1262 .unwrap_or_default()
1263 };
1264 input.parameters.insert(
1265 "AWS::NotificationARNs".to_string(),
1266 serde_json::to_string(&existing).unwrap_or_else(|_| "[]".to_string()),
1267 );
1268 }
1269
1270 let parsed = template::parse_template(&input.template_body, &input.parameters)
1275 .unwrap_or_else(|_| template::ParsedTemplate {
1276 description: None,
1277 resources: Vec::new(),
1278 outputs: Vec::new(),
1279 });
1280
1281 let imported_names = Self::validate_import_values(
1282 &self.state,
1283 &req.account_id,
1284 &input.stack_name,
1285 &input.template_body,
1286 &input.parameters,
1287 )?;
1288
1289 let provisioner = self.provisioner(&found_stack_id, &req.account_id, &req.region);
1290
1291 let mut accounts = self.state.write();
1292 let state = accounts.get_or_create(&req.account_id);
1293 let stack_exists = state.stacks.values().any(|s| {
1302 (s.name == input.stack_name || s.stack_id == input.stack_name)
1303 && s.status != "DELETE_COMPLETE"
1304 });
1305 if !stack_exists {
1306 let stack_id = if found_stack_id.is_empty() {
1307 format!(
1308 "arn:aws:cloudformation:{}:{}:stack/{}/{}",
1309 req.region,
1310 req.account_id,
1311 input.stack_name,
1312 uuid::Uuid::new_v4()
1313 )
1314 } else {
1315 found_stack_id.clone()
1316 };
1317 return Ok(AwsResponse::xml(
1318 StatusCode::OK,
1319 xml_responses::update_stack_response(&stack_id, &req.request_id),
1320 ));
1321 }
1322 let (update_result, stack_id, stack_name_owned, resources_snapshot, notification_arns) = {
1323 let stack = state
1324 .stacks
1325 .values_mut()
1326 .find(|s| {
1327 (s.name == input.stack_name || s.stack_id == input.stack_name)
1328 && s.status != "DELETE_COMPLETE"
1329 })
1330 .expect("stack existence checked above");
1331
1332 stack.status = "UPDATE_IN_PROGRESS".to_string();
1333 let update_result = apply_resource_updates(
1334 stack,
1335 &parsed.resources,
1336 &input.template_body,
1337 &input.parameters,
1338 &provisioner,
1339 );
1340
1341 let stack_id = stack.stack_id.clone();
1342 let stack_name_owned = stack.name.clone();
1343 stack.template = input.template_body.clone();
1344 stack.status = if update_result.is_err() {
1345 "UPDATE_ROLLBACK_COMPLETE".to_string()
1346 } else {
1347 "UPDATE_COMPLETE".to_string()
1348 };
1349 stack.parameters = input.parameters.clone();
1350 if !input.tags.is_empty() {
1351 stack.tags = input.tags;
1352 }
1353 stack.updated_at = Some(Utc::now());
1354 stack.description = parsed.description;
1355 if !input.notification_arns.is_empty() {
1356 stack.notification_arns = input.notification_arns.clone();
1357 }
1358 if update_result.is_ok() {
1359 stack.outputs.clear();
1360 }
1361 (
1362 update_result,
1363 stack_id,
1364 stack_name_owned,
1365 stack.resources.clone(),
1366 stack.notification_arns.clone(),
1367 )
1368 };
1369
1370 record_stack_status_event(
1372 state,
1373 &stack_id,
1374 &stack_name_owned,
1375 "AWS::CloudFormation::Stack",
1376 "UPDATE_IN_PROGRESS",
1377 );
1378 let update_result = match update_result {
1379 Ok(changes) => {
1380 record_stack_events(state, &stack_id, &stack_name_owned, &changes);
1381 record_stack_status_event(
1382 state,
1383 &stack_id,
1384 &stack_name_owned,
1385 "AWS::CloudFormation::Stack",
1386 "UPDATE_COMPLETE",
1387 );
1388 Ok(())
1389 }
1390 Err(e) => {
1391 record_stack_status_event(
1392 state,
1393 &stack_id,
1394 &stack_name_owned,
1395 "AWS::CloudFormation::Stack",
1396 "UPDATE_ROLLBACK_COMPLETE",
1397 );
1398 Err(e)
1399 }
1400 };
1401 let stack_name_for_notif = stack_name_owned.clone();
1402
1403 if let Err(error_msg) = update_result {
1404 drop(accounts);
1405 Self::send_stack_notification(
1406 &self.deps.delivery,
1407 ¬ification_arns,
1408 &stack_name_for_notif,
1409 &stack_id,
1410 "UPDATE_FAILED",
1411 );
1412 return Err(AwsServiceError::aws_error(
1413 StatusCode::BAD_REQUEST,
1414 "InsufficientCapabilitiesException",
1415 error_msg,
1416 ));
1417 }
1418
1419 drop(accounts);
1420
1421 let outputs = Self::resolve_template_outputs(
1422 &input.template_body,
1423 &input.parameters,
1424 &resources_snapshot,
1425 &self.state,
1426 );
1427 Self::ensure_export_uniqueness(&self.state, &req.account_id, &input.stack_name, &outputs)?;
1428 {
1429 let mut accounts = self.state.write();
1430 let state = accounts.get_or_create(&req.account_id);
1431 if let Some(stack) = state
1432 .stacks
1433 .values_mut()
1434 .find(|s| s.stack_id == stack_id && s.status != "DELETE_COMPLETE")
1435 {
1436 stack.outputs = outputs.clone();
1437 }
1438 Self::sync_exports_imports(
1439 state,
1440 &stack_id,
1441 &input.stack_name,
1442 &outputs,
1443 &imported_names,
1444 );
1445 }
1446
1447 Self::send_stack_notification(
1448 &self.deps.delivery,
1449 ¬ification_arns,
1450 &stack_name_for_notif,
1451 &stack_id,
1452 "UPDATE_COMPLETE",
1453 );
1454
1455 Ok(AwsResponse::xml(
1456 StatusCode::OK,
1457 xml_responses::update_stack_response(&stack_id, &req.request_id),
1458 ))
1459 }
1460
1461 fn get_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1462 let stack_name = Self::get_param(req, "StackName").unwrap_or_default();
1464
1465 let accounts = self.state.read();
1466 let empty = CloudFormationState::new(&req.account_id, &req.region);
1467 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1468 let body = state
1473 .stacks
1474 .values()
1475 .find(|s| {
1476 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
1477 })
1478 .map(|s| s.template.clone())
1479 .unwrap_or_default();
1480
1481 Ok(AwsResponse::xml(
1482 StatusCode::OK,
1483 xml_responses::get_template_response(&body, &req.request_id),
1484 ))
1485 }
1486}
1487
1488#[async_trait]
1489impl AwsService for CloudFormationService {
1490 fn service_name(&self) -> &str {
1491 "cloudformation"
1492 }
1493
1494 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1495 let action = req.action.as_str();
1496
1497 crate::input_constraints::validate_input(action, &Self::get_all_params(&req))?;
1504
1505 let mutates = matches!(
1509 action,
1510 "CreateStack"
1511 | "DeleteStack"
1512 | "UpdateStack"
1513 | "CreateChangeSet"
1514 | "DeleteChangeSet"
1515 | "ExecuteChangeSet"
1516 | "CreateStackSet"
1517 | "DeleteStackSet"
1518 | "CreateStackRefactor"
1519 | "CreateGeneratedTemplate"
1520 | "DeleteGeneratedTemplate"
1521 | "SetStackPolicy"
1522 | "UpdateTerminationProtection"
1523 | "ActivateOrganizationsAccess"
1524 | "DeactivateOrganizationsAccess"
1525 );
1526 let result = match action {
1527 "CreateStack" => self.create_stack(&req).await,
1528 "DeleteStack" => self.delete_stack(&req),
1529 "DescribeStacks" => self.describe_stacks(&req),
1530 "ListStacks" => self.list_stacks(&req),
1531 "ListStackResources" => self.list_stack_resources(&req),
1532 "DescribeStackResources" => self.describe_stack_resources(&req),
1533 "UpdateStack" => self.update_stack(&req),
1534 "GetTemplate" => self.get_template(&req),
1535 _ => self.handle_extra_action(&req),
1536 };
1537 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
1538 self.save_snapshot().await;
1539 }
1540 result
1541 }
1542
1543 fn supported_actions(&self) -> &[&str] {
1544 &[
1545 "ActivateOrganizationsAccess",
1546 "ActivateType",
1547 "BatchDescribeTypeConfigurations",
1548 "CancelUpdateStack",
1549 "ContinueUpdateRollback",
1550 "CreateChangeSet",
1551 "CreateGeneratedTemplate",
1552 "CreateStack",
1553 "CreateStackInstances",
1554 "CreateStackRefactor",
1555 "CreateStackSet",
1556 "DeactivateOrganizationsAccess",
1557 "DeactivateType",
1558 "DeleteChangeSet",
1559 "DeleteGeneratedTemplate",
1560 "DeleteStack",
1561 "DeleteStackInstances",
1562 "DeleteStackSet",
1563 "DeregisterType",
1564 "DescribeAccountLimits",
1565 "DescribeChangeSet",
1566 "DescribeChangeSetHooks",
1567 "DescribeEvents",
1568 "DescribeGeneratedTemplate",
1569 "DescribeOrganizationsAccess",
1570 "DescribePublisher",
1571 "DescribeResourceScan",
1572 "DescribeStackDriftDetectionStatus",
1573 "DescribeStackEvents",
1574 "DescribeStackInstance",
1575 "DescribeStackRefactor",
1576 "DescribeStackResource",
1577 "DescribeStackResourceDrifts",
1578 "DescribeStackResources",
1579 "DescribeStackSet",
1580 "DescribeStackSetOperation",
1581 "DescribeStacks",
1582 "DescribeType",
1583 "DescribeTypeRegistration",
1584 "DetectStackDrift",
1585 "DetectStackResourceDrift",
1586 "DetectStackSetDrift",
1587 "EstimateTemplateCost",
1588 "ExecuteChangeSet",
1589 "ExecuteStackRefactor",
1590 "GetGeneratedTemplate",
1591 "GetHookResult",
1592 "GetStackPolicy",
1593 "GetTemplate",
1594 "GetTemplateSummary",
1595 "ImportStacksToStackSet",
1596 "ListChangeSets",
1597 "ListExports",
1598 "ListGeneratedTemplates",
1599 "ListHookResults",
1600 "ListImports",
1601 "ListResourceScanRelatedResources",
1602 "ListResourceScanResources",
1603 "ListResourceScans",
1604 "ListStackInstanceResourceDrifts",
1605 "ListStackInstances",
1606 "ListStackRefactorActions",
1607 "ListStackRefactors",
1608 "ListStackResources",
1609 "ListStackSetAutoDeploymentTargets",
1610 "ListStackSetOperationResults",
1611 "ListStackSetOperations",
1612 "ListStackSets",
1613 "ListStacks",
1614 "ListTypeRegistrations",
1615 "ListTypeVersions",
1616 "ListTypes",
1617 "PublishType",
1618 "RecordHandlerProgress",
1619 "RegisterPublisher",
1620 "RegisterType",
1621 "RollbackStack",
1622 "SetStackPolicy",
1623 "SetTypeConfiguration",
1624 "SetTypeDefaultVersion",
1625 "SignalResource",
1626 "StartResourceScan",
1627 "StopStackSetOperation",
1628 "TestType",
1629 "UpdateGeneratedTemplate",
1630 "UpdateStack",
1631 "UpdateStackInstances",
1632 "UpdateStackSet",
1633 "UpdateTerminationProtection",
1634 "ValidateTemplate",
1635 ]
1636 }
1637}
1638
1639struct UpdateStackInput {
1641 stack_name: String,
1642 template_body: String,
1643 parameters: BTreeMap<String, String>,
1644 tags: BTreeMap<String, String>,
1645 notification_arns: Vec<String>,
1646}
1647
1648impl UpdateStackInput {
1649 fn from_params(req: &AwsRequest) -> Result<Self, AwsServiceError> {
1650 let params = CloudFormationService::get_all_params(req);
1651
1652 let stack_name = params
1653 .get("StackName")
1654 .ok_or_else(|| {
1655 AwsServiceError::aws_error(
1656 StatusCode::BAD_REQUEST,
1657 "ValidationError",
1658 "StackName is required",
1659 )
1660 })?
1661 .to_string();
1662
1663 let template_body = params.get("TemplateBody").cloned().unwrap_or_default();
1668
1669 Ok(Self {
1670 stack_name,
1671 template_body,
1672 parameters: CloudFormationService::extract_parameters(¶ms),
1673 tags: CloudFormationService::extract_tags(¶ms),
1674 notification_arns: CloudFormationService::extract_notification_arns(¶ms),
1675 })
1676 }
1677}
1678
1679#[derive(Debug, Clone)]
1683pub(crate) struct ResourceChange {
1684 pub action: ResourceChangeAction,
1685 pub logical_id: String,
1686 pub physical_id: String,
1687 pub resource_type: String,
1688}
1689
1690#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1691pub(crate) enum ResourceChangeAction {
1692 Create,
1693 Update,
1694 Delete,
1695}
1696
1697impl ResourceChangeAction {
1698 pub fn status_in_progress(self) -> &'static str {
1699 match self {
1700 Self::Create => "CREATE_IN_PROGRESS",
1701 Self::Update => "UPDATE_IN_PROGRESS",
1702 Self::Delete => "DELETE_IN_PROGRESS",
1703 }
1704 }
1705 pub fn status_complete(self) -> &'static str {
1706 match self {
1707 Self::Create => "CREATE_COMPLETE",
1708 Self::Update => "UPDATE_COMPLETE",
1709 Self::Delete => "DELETE_COMPLETE",
1710 }
1711 }
1712}
1713
1714pub(crate) fn apply_resource_updates(
1719 stack: &mut crate::state::Stack,
1720 new_resource_defs: &[template::ResourceDefinition],
1721 template_body: &str,
1722 parameters: &BTreeMap<String, String>,
1723 provisioner: &crate::resource_provisioner::ResourceProvisioner,
1724) -> Result<Vec<ResourceChange>, String> {
1725 let mut changes: Vec<ResourceChange> = Vec::new();
1726 let old_logical_ids: std::collections::HashSet<String> = stack
1727 .resources
1728 .iter()
1729 .map(|r| r.logical_id.clone())
1730 .collect();
1731 let new_logical_ids: std::collections::HashSet<String> = new_resource_defs
1732 .iter()
1733 .map(|r| r.logical_id.clone())
1734 .collect();
1735
1736 let to_remove: Vec<_> = stack
1738 .resources
1739 .iter()
1740 .filter(|r| !new_logical_ids.contains(&r.logical_id))
1741 .cloned()
1742 .collect();
1743 for resource in &to_remove {
1744 let _ = provisioner.delete_resource(resource);
1745 changes.push(ResourceChange {
1746 action: ResourceChangeAction::Delete,
1747 logical_id: resource.logical_id.clone(),
1748 physical_id: resource.physical_id.clone(),
1749 resource_type: resource.resource_type.clone(),
1750 });
1751 }
1752 stack
1753 .resources
1754 .retain(|r| new_logical_ids.contains(&r.logical_id));
1755
1756 let mut physical_ids: BTreeMap<String, String> = stack
1758 .resources
1759 .iter()
1760 .map(|r| (r.logical_id.clone(), r.physical_id.clone()))
1761 .collect();
1762 let mut attributes: BTreeMap<String, BTreeMap<String, String>> = stack
1763 .resources
1764 .iter()
1765 .map(|r| (r.logical_id.clone(), r.attributes.clone()))
1766 .collect();
1767
1768 let order = template::dependency_order(template_body, parameters, new_resource_defs);
1774 for &idx in &order {
1775 let resource_def = &new_resource_defs[idx];
1776 let resolved_def = template::resolve_resource_properties_with_attrs(
1777 resource_def,
1778 template_body,
1779 parameters,
1780 &physical_ids,
1781 &attributes,
1782 )
1783 .map_err(|e| {
1784 format!(
1785 "Failed to resolve resource {}: {e}",
1786 resource_def.logical_id
1787 )
1788 })?;
1789
1790 if !old_logical_ids.contains(&resource_def.logical_id) {
1791 match provisioner.create_resource(&resolved_def) {
1792 Ok(stack_resource) => {
1793 changes.push(ResourceChange {
1794 action: ResourceChangeAction::Create,
1795 logical_id: stack_resource.logical_id.clone(),
1796 physical_id: stack_resource.physical_id.clone(),
1797 resource_type: stack_resource.resource_type.clone(),
1798 });
1799 physical_ids.insert(
1800 stack_resource.logical_id.clone(),
1801 stack_resource.physical_id.clone(),
1802 );
1803 attributes.insert(
1804 stack_resource.logical_id.clone(),
1805 stack_resource.attributes.clone(),
1806 );
1807 stack.resources.push(stack_resource);
1808 }
1809 Err(e) => {
1810 tracing::warn!(
1811 "Failed to create resource {} during update: {e}",
1812 resource_def.logical_id
1813 );
1814 return Err(format!(
1815 "Failed to create resource {}: {e}",
1816 resource_def.logical_id
1817 ));
1818 }
1819 }
1820 } else {
1821 let existing = stack
1827 .resources
1828 .iter()
1829 .find(|r| r.logical_id == resource_def.logical_id)
1830 .cloned();
1831 if let Some(existing) = existing {
1832 match provisioner.update_resource(&existing, &resolved_def) {
1833 Ok(Some(updated)) => {
1834 changes.push(ResourceChange {
1835 action: ResourceChangeAction::Update,
1836 logical_id: updated.logical_id.clone(),
1837 physical_id: updated.physical_id.clone(),
1838 resource_type: updated.resource_type.clone(),
1839 });
1840 physical_ids
1841 .insert(updated.logical_id.clone(), updated.physical_id.clone());
1842 attributes.insert(updated.logical_id.clone(), updated.attributes.clone());
1843 if let Some(slot) = stack
1844 .resources
1845 .iter_mut()
1846 .find(|r| r.logical_id == updated.logical_id)
1847 {
1848 *slot = updated;
1849 }
1850 }
1851 Ok(None) => {
1852 }
1855 Err(e) => {
1856 tracing::warn!(
1857 "Failed to update resource {} during update: {e}",
1858 resource_def.logical_id
1859 );
1860 return Err(format!(
1861 "Failed to update resource {}: {e}",
1862 resource_def.logical_id
1863 ));
1864 }
1865 }
1866 }
1867 }
1868 }
1869
1870 Ok(changes)
1871}
1872
1873pub(crate) fn record_event(
1877 state: &mut crate::state::CloudFormationState,
1878 stack_id: &str,
1879 stack_name: &str,
1880 logical_id: &str,
1881 physical_id: &str,
1882 resource_type: &str,
1883 status: &str,
1884) {
1885 use serde_json::json;
1886 let event_id = format!(
1887 "{}-{:x}",
1888 logical_id,
1889 std::time::SystemTime::now()
1890 .duration_since(std::time::UNIX_EPOCH)
1891 .map(|d| d.as_nanos())
1892 .unwrap_or(0)
1893 );
1894 let log = state.events.entry(stack_id.to_string()).or_default();
1895
1896 let now = chrono::DateTime::from_timestamp_millis(Utc::now().timestamp_millis())
1909 .unwrap_or_else(Utc::now);
1910 let timestamp = match log.last().and_then(|e| e["Timestamp"].as_str()) {
1911 Some(prev) => match chrono::DateTime::parse_from_rfc3339(prev) {
1912 Ok(prev) => {
1913 let prev = prev.with_timezone(&Utc);
1914 if now > prev {
1915 now
1916 } else {
1917 prev + chrono::Duration::milliseconds(1)
1918 }
1919 }
1920 Err(_) => now,
1921 },
1922 None => now,
1923 };
1924
1925 log.push(json!({
1926 "EventId": event_id,
1927 "StackId": stack_id,
1928 "StackName": stack_name,
1929 "LogicalResourceId": logical_id,
1930 "PhysicalResourceId": physical_id,
1931 "ResourceType": resource_type,
1932 "ResourceStatus": status,
1933 "Timestamp": timestamp.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
1934 }));
1935}
1936
1937async fn save_snapshot_static(
1945 state: SharedCloudFormationState,
1946 store: Option<Arc<dyn SnapshotStore>>,
1947 lock: Arc<AsyncMutex<()>>,
1948) {
1949 let Some(store) = store else {
1950 return;
1951 };
1952 let _guard = lock.lock().await;
1953 let snapshot = CloudFormationSnapshot {
1954 schema_version: CLOUDFORMATION_SNAPSHOT_SCHEMA_VERSION,
1955 state: None,
1956 accounts: Some(state.read().clone()),
1957 };
1958 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
1959 let bytes = serde_json::to_vec(&snapshot)
1960 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
1961 store.save(&bytes)
1962 })
1963 .await;
1964 match join {
1965 Ok(Ok(())) => {}
1966 Ok(Err(err)) => tracing::error!(%err, "failed to write cloudformation snapshot"),
1967 Err(err) => tracing::error!(%err, "cloudformation snapshot task panicked"),
1968 }
1969}
1970
1971pub(crate) fn record_stack_events(
1972 state: &mut crate::state::CloudFormationState,
1973 stack_id: &str,
1974 stack_name: &str,
1975 changes: &[ResourceChange],
1976) {
1977 for ch in changes {
1978 record_event(
1979 state,
1980 stack_id,
1981 stack_name,
1982 &ch.logical_id,
1983 &ch.physical_id,
1984 &ch.resource_type,
1985 ch.action.status_in_progress(),
1986 );
1987 record_event(
1988 state,
1989 stack_id,
1990 stack_name,
1991 &ch.logical_id,
1992 &ch.physical_id,
1993 &ch.resource_type,
1994 ch.action.status_complete(),
1995 );
1996 }
1997}
1998
1999pub(crate) fn record_stack_status_event(
2003 state: &mut crate::state::CloudFormationState,
2004 stack_id: &str,
2005 stack_name: &str,
2006 resource_type: &str,
2007 status: &str,
2008) {
2009 record_event(
2010 state,
2011 stack_id,
2012 stack_name,
2013 stack_name,
2014 stack_id,
2015 resource_type,
2016 status,
2017 );
2018}
2019
2020#[cfg(test)]
2021mod tests {
2022 use super::*;
2023 use http::HeaderMap;
2024 use parking_lot::RwLock;
2025 use std::collections::HashMap;
2026 use std::sync::Arc;
2027
2028 fn make_service() -> CloudFormationService {
2029 let cf_state = Arc::new(RwLock::new(
2030 fakecloud_core::multi_account::MultiAccountState::new(
2031 "123456789012",
2032 "us-east-1",
2033 "http://localhost:4566",
2034 ),
2035 ));
2036 let deps = CloudFormationDeps {
2037 sqs: Arc::new(RwLock::new(
2038 fakecloud_core::multi_account::MultiAccountState::new(
2039 "123456789012",
2040 "us-east-1",
2041 "http://localhost:4566",
2042 ),
2043 )),
2044 sns: Arc::new(RwLock::new(
2045 fakecloud_core::multi_account::MultiAccountState::new(
2046 "123456789012",
2047 "us-east-1",
2048 "http://localhost:4566",
2049 ),
2050 )),
2051 ssm: Arc::new(RwLock::new(
2052 fakecloud_core::multi_account::MultiAccountState::new(
2053 "123456789012",
2054 "us-east-1",
2055 "http://localhost:4566",
2056 ),
2057 )),
2058 iam: Arc::new(RwLock::new(
2059 fakecloud_core::multi_account::MultiAccountState::new(
2060 "123456789012",
2061 "us-east-1",
2062 "",
2063 ),
2064 )),
2065 s3: Arc::new(RwLock::new(
2066 fakecloud_core::multi_account::MultiAccountState::new(
2067 "123456789012",
2068 "us-east-1",
2069 "",
2070 ),
2071 )),
2072 eventbridge: Arc::new(RwLock::new(
2073 fakecloud_core::multi_account::MultiAccountState::new(
2074 "123456789012",
2075 "us-east-1",
2076 "",
2077 ),
2078 )),
2079 dynamodb: Arc::new(RwLock::new(
2080 fakecloud_core::multi_account::MultiAccountState::new(
2081 "123456789012",
2082 "us-east-1",
2083 "",
2084 ),
2085 )),
2086 logs: Arc::new(RwLock::new(
2087 fakecloud_core::multi_account::MultiAccountState::new(
2088 "123456789012",
2089 "us-east-1",
2090 "",
2091 ),
2092 )),
2093 lambda: Arc::new(RwLock::new(
2094 fakecloud_core::multi_account::MultiAccountState::new(
2095 "123456789012",
2096 "us-east-1",
2097 "",
2098 ),
2099 )),
2100 secretsmanager: Arc::new(RwLock::new(
2101 fakecloud_core::multi_account::MultiAccountState::new(
2102 "123456789012",
2103 "us-east-1",
2104 "",
2105 ),
2106 )),
2107 kinesis: Arc::new(RwLock::new(
2108 fakecloud_core::multi_account::MultiAccountState::new(
2109 "123456789012",
2110 "us-east-1",
2111 "",
2112 ),
2113 )),
2114 kms: Arc::new(RwLock::new(
2115 fakecloud_core::multi_account::MultiAccountState::new(
2116 "123456789012",
2117 "us-east-1",
2118 "",
2119 ),
2120 )),
2121 ecr: Arc::new(RwLock::new(
2122 fakecloud_core::multi_account::MultiAccountState::new(
2123 "123456789012",
2124 "us-east-1",
2125 "",
2126 ),
2127 )),
2128 cloudwatch: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
2129 elbv2: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
2130 organizations: Arc::new(RwLock::new(None)),
2131 cognito: Arc::new(RwLock::new(
2132 fakecloud_core::multi_account::MultiAccountState::new(
2133 "123456789012",
2134 "us-east-1",
2135 "",
2136 ),
2137 )),
2138 rds: Arc::new(RwLock::new(
2139 fakecloud_core::multi_account::MultiAccountState::new(
2140 "123456789012",
2141 "us-east-1",
2142 "",
2143 ),
2144 )),
2145 ecs: Arc::new(RwLock::new(
2146 fakecloud_core::multi_account::MultiAccountState::new(
2147 "123456789012",
2148 "us-east-1",
2149 "",
2150 ),
2151 )),
2152 acm: Arc::new(RwLock::new(fakecloud_acm::AcmAccounts::new())),
2153 elasticache: Arc::new(RwLock::new(
2154 fakecloud_core::multi_account::MultiAccountState::new(
2155 "123456789012",
2156 "us-east-1",
2157 "",
2158 ),
2159 )),
2160 route53: Arc::new(RwLock::new(fakecloud_route53::Route53Accounts::new())),
2161 cloudfront: Arc::new(RwLock::new(fakecloud_cloudfront::CloudFrontAccounts::new())),
2162 stepfunctions: Arc::new(RwLock::new(
2163 fakecloud_core::multi_account::MultiAccountState::new(
2164 "123456789012",
2165 "us-east-1",
2166 "",
2167 ),
2168 )),
2169 wafv2: Arc::new(RwLock::new(fakecloud_wafv2::Wafv2Accounts::default())),
2170 apigateway: Arc::new(RwLock::new(
2171 fakecloud_core::multi_account::MultiAccountState::new(
2172 "123456789012",
2173 "us-east-1",
2174 "",
2175 ),
2176 )),
2177 apigatewayv2: Arc::new(RwLock::new(
2178 fakecloud_core::multi_account::MultiAccountState::new(
2179 "123456789012",
2180 "us-east-1",
2181 "",
2182 ),
2183 )),
2184 ses: Arc::new(RwLock::new(
2185 fakecloud_core::multi_account::MultiAccountState::new(
2186 "123456789012",
2187 "us-east-1",
2188 "",
2189 ),
2190 )),
2191 application_autoscaling: Arc::new(parking_lot::RwLock::new(
2192 fakecloud_application_autoscaling::ApplicationAutoScalingAccounts::new(),
2193 )),
2194 athena: Arc::new(parking_lot::RwLock::new(
2195 fakecloud_athena::AthenaAccounts::new(),
2196 )),
2197 firehose: Arc::new(parking_lot::RwLock::new(
2198 fakecloud_firehose::FirehoseAccounts::new(),
2199 )),
2200 glue: Arc::new(parking_lot::RwLock::new(fakecloud_glue::GlueAccounts::new())),
2201 delivery: Arc::new(DeliveryBus::new()),
2202 lambda_runtime: None,
2203 };
2204 CloudFormationService::new(cf_state, deps)
2205 }
2206
2207 fn make_request(action: &str, params: HashMap<String, String>) -> AwsRequest {
2208 AwsRequest {
2209 service: "cloudformation".to_string(),
2210 action: action.to_string(),
2211 region: "us-east-1".to_string(),
2212 account_id: "123456789012".to_string(),
2213 request_id: "test-request-id".to_string(),
2214 headers: HeaderMap::new(),
2215 query_params: params,
2216 body: bytes::Bytes::new(),
2217 body_stream: parking_lot::Mutex::new(None),
2218 path_segments: vec![],
2219 raw_path: "/".to_string(),
2220 raw_query: String::new(),
2221 method: http::Method::POST,
2222 is_query_protocol: true,
2223 access_key_id: None,
2224 principal: None,
2225 }
2226 }
2227
2228 #[tokio::test]
2229 async fn update_stack_sets_failed_status_on_resource_error() {
2230 let svc = make_service();
2231
2232 let mut create_params = HashMap::new();
2234 create_params.insert("StackName".to_string(), "test-stack".to_string());
2235 create_params.insert(
2236 "TemplateBody".to_string(),
2237 r#"{"Resources":{"MyQueue":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"q1"}}}}"#.to_string(),
2238 );
2239 let req = make_request("CreateStack", create_params);
2240 let result = svc.create_stack(&req).await;
2241 assert!(result.is_ok());
2242
2243 let mut update_params = HashMap::new();
2245 update_params.insert("StackName".to_string(), "test-stack".to_string());
2246 update_params.insert(
2247 "TemplateBody".to_string(),
2248 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(),
2249 );
2250 let req = make_request("UpdateStack", update_params);
2251 let result = svc.update_stack(&req);
2252
2253 assert!(result.is_err());
2255
2256 let accounts = svc.state.read();
2260 let state = accounts.get("123456789012").unwrap();
2261 let stack = state.stacks.get("test-stack").unwrap();
2262 assert_eq!(stack.status, "UPDATE_ROLLBACK_COMPLETE");
2263 }
2264
2265 #[tokio::test]
2266 async fn create_stack_resolves_ref_to_physical_id() {
2267 let svc = make_service();
2268
2269 let template = r#"{
2271 "Resources": {
2272 "MyTopic": {
2273 "Type": "AWS::SNS::Topic",
2274 "Properties": { "TopicName": "ref-test-topic" }
2275 },
2276 "MySub": {
2277 "Type": "AWS::SNS::Subscription",
2278 "Properties": {
2279 "TopicArn": { "Ref": "MyTopic" },
2280 "Protocol": "sqs",
2281 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:some-queue"
2282 }
2283 }
2284 }
2285 }"#;
2286
2287 let mut params = HashMap::new();
2288 params.insert("StackName".to_string(), "ref-stack".to_string());
2289 params.insert("TemplateBody".to_string(), template.to_string());
2290 let req = make_request("CreateStack", params);
2291 let result = svc.create_stack(&req).await;
2292 assert!(result.is_ok(), "CreateStack failed: {:?}", result.err());
2293
2294 let accounts = svc.state.read();
2296 let state = accounts.get("123456789012").unwrap();
2297 let stack = state.stacks.get("ref-stack").unwrap();
2298 assert_eq!(stack.resources.len(), 2);
2299 assert_eq!(stack.status, "CREATE_COMPLETE");
2300
2301 let sub = stack
2303 .resources
2304 .iter()
2305 .find(|r| r.logical_id == "MySub")
2306 .unwrap();
2307 assert!(
2308 sub.physical_id.contains("ref-test-topic"),
2309 "Subscription physical ID should reference the topic ARN, got: {}",
2310 sub.physical_id
2311 );
2312 }
2313
2314 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2321 async fn create_stack_custom_resource_provisions_asynchronously() {
2322 let svc = make_service();
2323 let template = r#"{
2324 "Resources": {
2325 "MyCustom": {
2326 "Type": "Custom::Thing",
2327 "Properties": {
2328 "ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:handler"
2329 }
2330 }
2331 }
2332 }"#;
2333 let mut params = HashMap::new();
2334 params.insert("StackName".to_string(), "async-stack".to_string());
2335 params.insert("TemplateBody".to_string(), template.to_string());
2336 let req = make_request("CreateStack", params);
2337
2338 let resp = svc
2345 .create_stack(&req)
2346 .await
2347 .expect("create returns StackId");
2348 assert!(resp.status.is_success());
2349 {
2350 let accounts = svc.state.read();
2351 let stack = accounts
2352 .get("123456789012")
2353 .unwrap()
2354 .stacks
2355 .get("async-stack")
2356 .expect("stack seeded synchronously");
2357 assert!(
2358 stack.status == "CREATE_IN_PROGRESS" || stack.status == "CREATE_COMPLETE",
2359 "unexpected status right after create: {}",
2360 stack.status
2361 );
2362 }
2363
2364 let mut status = String::new();
2367 for _ in 0..200 {
2368 {
2369 let accounts = svc.state.read();
2370 if let Some(stack) = accounts
2371 .get("123456789012")
2372 .and_then(|s| s.stacks.get("async-stack"))
2373 {
2374 status = stack.status.clone();
2375 if status != "CREATE_IN_PROGRESS" {
2376 break;
2377 }
2378 }
2379 }
2380 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2381 }
2382 assert_eq!(
2383 status, "CREATE_COMPLETE",
2384 "stack should reach CREATE_COMPLETE"
2385 );
2386
2387 let accounts = svc.state.read();
2388 let stack = accounts
2389 .get("123456789012")
2390 .unwrap()
2391 .stacks
2392 .get("async-stack")
2393 .unwrap();
2394 assert_eq!(stack.resources.len(), 1);
2395 assert_eq!(stack.resources[0].resource_type, "Custom::Thing");
2396 }
2397
2398 #[tokio::test]
2401 async fn create_stack_missing_name_errors() {
2402 let svc = make_service();
2403 let mut params = HashMap::new();
2404 params.insert("TemplateBody".to_string(), "{}".to_string());
2405 let req = make_request("CreateStack", params);
2406 assert!(svc.create_stack(&req).await.is_err());
2407 }
2408
2409 #[tokio::test]
2410 async fn create_stack_missing_template_creates_empty_stack() {
2411 let svc = make_service();
2416 let mut params = HashMap::new();
2417 params.insert("StackName".to_string(), "s".to_string());
2418 let req = make_request("CreateStack", params);
2419 svc.create_stack(&req)
2420 .await
2421 .expect("empty-body create succeeds");
2422 }
2423
2424 #[tokio::test]
2425 async fn create_stack_duplicate_errors() {
2426 let svc = make_service();
2427 let mut params = HashMap::new();
2428 params.insert("StackName".to_string(), "dup".to_string());
2429 params.insert(
2430 "TemplateBody".to_string(),
2431 r#"{"Resources":{"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"dq"}}}}"#
2432 .to_string(),
2433 );
2434 let req = make_request("CreateStack", params.clone());
2435 svc.create_stack(&req).await.unwrap();
2436 let req = make_request("CreateStack", params);
2437 assert!(svc.create_stack(&req).await.is_err());
2438 }
2439
2440 #[tokio::test]
2441 async fn create_stack_invalid_template_creates_empty_stack() {
2442 let svc = make_service();
2446 let mut params = HashMap::new();
2447 params.insert("StackName".to_string(), "bad".to_string());
2448 params.insert("TemplateBody".to_string(), "not json".to_string());
2449 let req = make_request("CreateStack", params);
2450 svc.create_stack(&req)
2451 .await
2452 .expect("bad-body create succeeds");
2453 }
2454
2455 #[test]
2456 fn delete_stack_unknown_is_noop() {
2457 let svc = make_service();
2458 let mut params = HashMap::new();
2459 params.insert("StackName".to_string(), "ghost".to_string());
2460 let req = make_request("DeleteStack", params);
2461 assert!(svc.delete_stack(&req).is_ok());
2462 }
2463
2464 #[test]
2465 fn describe_stacks_nonexistent_errors() {
2466 let svc = make_service();
2471 let mut params = HashMap::new();
2472 params.insert("StackName".to_string(), "ghost".to_string());
2473 let req = make_request("DescribeStacks", params);
2474 match svc.describe_stacks(&req) {
2475 Ok(_) => panic!("ghost stack must return an error, not an empty list"),
2476 Err(e) => {
2477 assert_eq!(e.status(), StatusCode::BAD_REQUEST);
2478 assert_eq!(e.code(), "ValidationError");
2479 assert!(
2480 e.message().contains("does not exist"),
2481 "got: {}",
2482 e.message()
2483 );
2484 }
2485 }
2486 }
2487
2488 #[test]
2489 fn describe_stacks_empty_returns_all() {
2490 let svc = make_service();
2491 let req = make_request("DescribeStacks", HashMap::new());
2492 let resp = svc.describe_stacks(&req).unwrap();
2493 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
2494 assert!(b.contains("DescribeStacksResult"));
2495 }
2496
2497 #[test]
2498 fn list_stacks_empty_returns_ok() {
2499 let svc = make_service();
2500 let req = make_request("ListStacks", HashMap::new());
2501 let resp = svc.list_stacks(&req).unwrap();
2502 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
2503 assert!(b.contains("ListStacksResult"));
2504 }
2505
2506 #[test]
2507 fn list_stack_resources_missing_name_returns_validation_error() {
2508 let svc = make_service();
2514 let req = make_request("ListStackResources", HashMap::new());
2515 let err = match svc.list_stack_resources(&req) {
2516 Err(e) => e,
2517 Ok(_) => panic!("omitted StackName must be rejected"),
2518 };
2519 assert_eq!(err.code(), "ValidationError");
2520 }
2521
2522 #[test]
2523 fn list_stack_resources_unknown_stack_returns_empty() {
2524 let svc = make_service();
2525 let mut params = HashMap::new();
2526 params.insert("StackName".to_string(), "ghost".to_string());
2527 let req = make_request("ListStackResources", params);
2528 svc.list_stack_resources(&req).expect("unknown is empty");
2529 }
2530
2531 #[test]
2532 fn describe_stack_resources_missing_name_returns_empty() {
2533 let svc = make_service();
2534 let req = make_request("DescribeStackResources", HashMap::new());
2535 svc.describe_stack_resources(&req)
2536 .expect("missing name is ok");
2537 }
2538
2539 #[test]
2540 fn get_template_missing_name_returns_empty_body() {
2541 let svc = make_service();
2542 let req = make_request("GetTemplate", HashMap::new());
2543 svc.get_template(&req).expect("missing name is ok");
2544 }
2545
2546 #[test]
2547 fn get_template_unknown_stack_returns_empty_body() {
2548 let svc = make_service();
2549 let mut params = HashMap::new();
2550 params.insert("StackName".to_string(), "ghost".to_string());
2551 let req = make_request("GetTemplate", params);
2552 svc.get_template(&req).expect("unknown is empty");
2553 }
2554
2555 #[test]
2556 fn update_stack_missing_name_errors() {
2557 let svc = make_service();
2558 let mut params = HashMap::new();
2559 params.insert("TemplateBody".to_string(), "{}".to_string());
2560 let req = make_request("UpdateStack", params);
2561 assert!(svc.update_stack(&req).is_err());
2562 }
2563
2564 #[test]
2565 fn update_stack_unknown_stack_returns_synthetic_id() {
2566 let svc = make_service();
2573 let mut params = HashMap::new();
2574 params.insert("StackName".to_string(), "ghost".to_string());
2575 params.insert(
2576 "TemplateBody".to_string(),
2577 r#"{"Resources":{}}"#.to_string(),
2578 );
2579 let req = make_request("UpdateStack", params);
2580 let resp = svc.update_stack(&req).expect("ghost update is synthetic");
2581 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
2582 assert!(b.contains("UpdateStackResult"));
2583 }
2584
2585 #[tokio::test]
2586 async fn create_stack_resolves_outputs_and_records_export() {
2587 let svc = make_service();
2588 let template = r#"{
2589 "Resources": {
2590 "Q": {"Type":"AWS::SQS::Queue","Properties":{"QueueName":"out-q"}}
2591 },
2592 "Outputs": {
2593 "QueueUrl": {
2594 "Value": {"Ref": "Q"},
2595 "Description": "Url",
2596 "Export": {"Name": "TheQueueUrl"}
2597 }
2598 }
2599 }"#;
2600 let mut params = HashMap::new();
2601 params.insert("StackName".to_string(), "outs".to_string());
2602 params.insert("TemplateBody".to_string(), template.to_string());
2603 let req = make_request("CreateStack", params);
2604 svc.create_stack(&req).await.expect("create stack");
2605
2606 let accounts = svc.state.read();
2607 let stack = accounts
2608 .get("123456789012")
2609 .unwrap()
2610 .stacks
2611 .get("outs")
2612 .unwrap();
2613 assert_eq!(stack.outputs.len(), 1);
2614 assert_eq!(stack.outputs[0].key, "QueueUrl");
2615 assert_eq!(stack.outputs[0].export_name.as_deref(), Some("TheQueueUrl"));
2616 assert!(!stack.outputs[0].value.is_empty());
2617 }
2618
2619 #[tokio::test]
2620 async fn create_stack_rejects_duplicate_export_name() {
2621 let svc = make_service();
2622 let mk = |name: &str| {
2623 let template = format!(
2624 r#"{{
2625 "Resources": {{"Q":{{"Type":"AWS::SQS::Queue","Properties":{{"QueueName":"q-{name}"}}}}}},
2626 "Outputs": {{"QueueUrl":{{"Value":{{"Ref":"Q"}},"Export":{{"Name":"DupExport"}}}}}}
2627 }}"#
2628 );
2629 let mut params = HashMap::new();
2630 params.insert("StackName".to_string(), name.to_string());
2631 params.insert("TemplateBody".to_string(), template);
2632 make_request("CreateStack", params)
2633 };
2634 match svc.create_stack(&mk("first")).await {
2635 Ok(_) => {}
2636 Err(e) => panic!("first stack: {e:?}"),
2637 }
2638 svc.create_stack(&mk("second"))
2644 .await
2645 .expect("CreateStack returns StackId even when provisioning fails");
2646 let accounts = svc.state.read();
2647 let stack = accounts
2648 .get("123456789012")
2649 .unwrap()
2650 .stacks
2651 .get("second")
2652 .expect("second stack recorded");
2653 assert_eq!(stack.status, "CREATE_FAILED");
2654 let exports = &accounts.get("123456789012").unwrap().exports;
2656 assert_eq!(
2657 exports
2658 .get("DupExport")
2659 .map(|e| e.exporting_stack_name.as_str()),
2660 Some("first")
2661 );
2662 }
2663
2664 #[tokio::test]
2665 async fn import_value_resolves_against_other_stack_export() {
2666 let svc = make_service();
2667
2668 let producer_tpl = r#"{
2669 "Resources": {"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"prod-q"}}},
2670 "Outputs": {"Out":{"Value":{"Ref":"Q"},"Export":{"Name":"SharedQueueUrl"}}}
2671 }"#;
2672 let mut p = HashMap::new();
2673 p.insert("StackName".to_string(), "producer".to_string());
2674 p.insert("TemplateBody".to_string(), producer_tpl.to_string());
2675 svc.create_stack(&make_request("CreateStack", p))
2676 .await
2677 .expect("producer");
2678
2679 let consumer_tpl = r#"{
2680 "Resources": {"Q2":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"cons-q"}}},
2681 "Outputs": {"Imp":{"Value":{"Fn::ImportValue":"SharedQueueUrl"}}}
2682 }"#;
2683 let mut p = HashMap::new();
2684 p.insert("StackName".to_string(), "consumer".to_string());
2685 p.insert("TemplateBody".to_string(), consumer_tpl.to_string());
2686 svc.create_stack(&make_request("CreateStack", p))
2687 .await
2688 .expect("consumer");
2689
2690 let accounts = svc.state.read();
2691 let prod_url = accounts
2692 .get("123456789012")
2693 .unwrap()
2694 .stacks
2695 .get("producer")
2696 .unwrap()
2697 .outputs[0]
2698 .value
2699 .clone();
2700 let cons = accounts
2701 .get("123456789012")
2702 .unwrap()
2703 .stacks
2704 .get("consumer")
2705 .unwrap();
2706 assert_eq!(cons.outputs[0].value, prod_url);
2707 }
2708
2709 #[tokio::test]
2710 async fn create_stack_records_export_in_state_registry() {
2711 let svc = make_service();
2712 let template = r#"{
2713 "Resources": {"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"reg-q"}}},
2714 "Outputs": {"Url":{"Value":{"Ref":"Q"},"Export":{"Name":"reg-url"}}}
2715 }"#;
2716 let mut params = HashMap::new();
2717 params.insert("StackName".to_string(), "reg".to_string());
2718 params.insert("TemplateBody".to_string(), template.to_string());
2719 svc.create_stack(&make_request("CreateStack", params))
2720 .await
2721 .expect("create");
2722
2723 let accounts = svc.state.read();
2724 let state = accounts.get("123456789012").unwrap();
2725 let export = state
2726 .exports
2727 .get("reg-url")
2728 .expect("export registered in state.exports");
2729 assert_eq!(export.exporting_stack_name, "reg");
2730 assert!(!export.value.is_empty());
2731 assert!(export.exporting_stack_id.contains("reg"));
2732 }
2733
2734 #[tokio::test]
2735 async fn import_value_with_unknown_export_errors() {
2736 let svc = make_service();
2737 let consumer_tpl = r#"{
2738 "Resources": {"Q":{"Type":"AWS::SQS::Queue","Properties":{
2739 "QueueName": {"Fn::ImportValue":"missing-export"}
2740 }}}
2741 }"#;
2742 let mut p = HashMap::new();
2743 p.insert("StackName".to_string(), "bad-consumer".to_string());
2744 p.insert("TemplateBody".to_string(), consumer_tpl.to_string());
2745 match svc.create_stack(&make_request("CreateStack", p)).await {
2746 Ok(_) => panic!("expected ValidationError for unknown export"),
2747 Err(e) => {
2748 let msg = format!("{e:?}");
2749 assert!(msg.contains("No export named missing-export"), "got {msg}");
2750 }
2751 }
2752 }
2753
2754 #[tokio::test]
2755 async fn delete_stack_blocked_when_export_in_use_and_unblocked_after_consumer_delete() {
2756 let svc = make_service();
2757
2758 let producer_tpl = r#"{
2759 "Resources": {"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"prod"}}},
2760 "Outputs": {"Out":{"Value":{"Ref":"Q"},"Export":{"Name":"my-arn"}}}
2761 }"#;
2762 let mut p = HashMap::new();
2763 p.insert("StackName".to_string(), "producer".to_string());
2764 p.insert("TemplateBody".to_string(), producer_tpl.to_string());
2765 svc.create_stack(&make_request("CreateStack", p))
2766 .await
2767 .expect("producer");
2768
2769 let consumer_tpl = r#"{
2770 "Resources": {"Q2":{"Type":"AWS::SQS::Queue","Properties":{
2771 "QueueName": "cons-q",
2772 "Tags": [{"Key":"k","Value":{"Fn::ImportValue":"my-arn"}}]
2773 }}}
2774 }"#;
2775 let mut p = HashMap::new();
2776 p.insert("StackName".to_string(), "consumer".to_string());
2777 p.insert("TemplateBody".to_string(), consumer_tpl.to_string());
2778 svc.create_stack(&make_request("CreateStack", p))
2779 .await
2780 .expect("consumer");
2781
2782 let mut p = HashMap::new();
2784 p.insert("StackName".to_string(), "producer".to_string());
2785 match svc.delete_stack(&make_request("DeleteStack", p)) {
2786 Ok(_) => panic!("delete must fail while imports exist"),
2787 Err(e) => {
2788 let msg = format!("{e:?}");
2789 assert!(msg.contains("Export my-arn cannot be deleted"), "got {msg}");
2790 }
2791 }
2792
2793 let mut p = HashMap::new();
2795 p.insert("StackName".to_string(), "consumer".to_string());
2796 svc.delete_stack(&make_request("DeleteStack", p))
2797 .expect("consumer delete");
2798
2799 let mut p = HashMap::new();
2801 p.insert("StackName".to_string(), "producer".to_string());
2802 svc.delete_stack(&make_request("DeleteStack", p))
2803 .expect("producer delete after consumer gone");
2804
2805 let accounts = svc.state.read();
2806 let state = accounts.get("123456789012").unwrap();
2807 assert!(state.exports.is_empty(), "exports cleared after delete");
2808 assert!(state.imports.is_empty(), "imports cleared after delete");
2809 }
2810}