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 mut pending: Vec<&template::ResourceDefinition> = resource_defs.iter().collect();
72 let max_passes = pending.len() + 1;
73
74 for _ in 0..max_passes {
75 if pending.is_empty() {
76 break;
77 }
78 let mut still_pending = Vec::new();
79 let mut made_progress = false;
80
81 for resource_def in pending {
82 let resolved_def = template::resolve_resource_properties_with_attrs(
83 resource_def,
84 template_body,
85 parameters,
86 &physical_ids,
87 &attributes,
88 )
89 .map_err(|e| {
90 AwsServiceError::aws_error(
94 StatusCode::BAD_REQUEST,
95 "InsufficientCapabilitiesException",
96 e,
97 )
98 })?;
99
100 match provisioner.create_resource(&resolved_def) {
101 Ok(stack_resource) => {
102 physical_ids.insert(
103 stack_resource.logical_id.clone(),
104 stack_resource.physical_id.clone(),
105 );
106 let mut attr_map = stack_resource.attributes.clone();
111 for attr in well_known_attributes_for(&stack_resource.resource_type) {
112 if attr_map.contains_key(*attr) {
113 continue;
114 }
115 if let Some(v) = provisioner.get_att(&stack_resource, attr) {
116 attr_map.insert((*attr).to_string(), v);
117 }
118 }
119 attributes.insert(stack_resource.logical_id.clone(), attr_map);
120 resources.push(stack_resource);
121 made_progress = true;
122 }
123 Err(_) => still_pending.push(resource_def),
124 }
125 }
126
127 pending = still_pending;
128 if !made_progress && !pending.is_empty() {
129 let resource_def = pending[0];
132 let resolved_def = template::resolve_resource_properties_with_attrs(
133 resource_def,
134 template_body,
135 parameters,
136 &physical_ids,
137 &attributes,
138 )
139 .unwrap_or_else(|_| resource_def.clone());
140 let err = provisioner.create_resource(&resolved_def).unwrap_err();
141 for r in &resources {
142 let _ = provisioner.delete_resource(r);
143 }
144 return Err(AwsServiceError::aws_error(
145 StatusCode::BAD_REQUEST,
146 "ValidationError",
147 format!(
148 "Failed to create resource {}: {err}",
149 resource_def.logical_id
150 ),
151 ));
152 }
153 }
154
155 Ok(resources)
156}
157
158pub struct CloudFormationDeps {
160 pub sqs: SharedSqsState,
161 pub sns: SharedSnsState,
162 pub ssm: SharedSsmState,
163 pub iam: SharedIamState,
164 pub s3: SharedS3State,
165 pub eventbridge: SharedEventBridgeState,
166 pub dynamodb: SharedDynamoDbState,
167 pub logs: SharedLogsState,
168 pub lambda: fakecloud_lambda::SharedLambdaState,
169 pub secretsmanager: fakecloud_secretsmanager::SharedSecretsManagerState,
170 pub kinesis: fakecloud_kinesis::SharedKinesisState,
171 pub kms: fakecloud_kms::SharedKmsState,
172 pub ecr: fakecloud_ecr::SharedEcrState,
173 pub cloudwatch: fakecloud_cloudwatch::SharedCloudWatchState,
174 pub elbv2: fakecloud_elbv2::SharedElbv2State,
175 pub organizations: fakecloud_organizations::SharedOrganizationsState,
176 pub cognito: fakecloud_cognito::SharedCognitoState,
177 pub rds: fakecloud_rds::SharedRdsState,
178 pub ecs: fakecloud_ecs::SharedEcsState,
179 pub acm: fakecloud_acm::SharedAcmState,
180 pub elasticache: fakecloud_elasticache::SharedElastiCacheState,
181 pub route53: fakecloud_route53::SharedRoute53State,
182 pub cloudfront: fakecloud_cloudfront::SharedCloudFrontState,
183 pub stepfunctions: fakecloud_stepfunctions::SharedStepFunctionsState,
184 pub wafv2: fakecloud_wafv2::SharedWafv2State,
185 pub apigateway: fakecloud_apigateway::SharedApiGatewayState,
186 pub apigatewayv2: fakecloud_apigatewayv2::SharedApiGatewayV2State,
187 pub ses: fakecloud_ses::SharedSesState,
188 pub application_autoscaling:
189 fakecloud_application_autoscaling::SharedApplicationAutoScalingState,
190 pub athena: fakecloud_athena::SharedAthenaState,
191 pub firehose: fakecloud_firehose::SharedFirehoseState,
192 pub glue: fakecloud_glue::SharedGlueState,
193 pub delivery: Arc<DeliveryBus>,
194 pub lambda_runtime: Option<Arc<fakecloud_lambda::runtime::ContainerRuntime>>,
201}
202
203pub struct CloudFormationService {
204 pub(crate) state: SharedCloudFormationState,
205 pub(crate) deps: CloudFormationDeps,
206 snapshot_store: Option<Arc<dyn SnapshotStore>>,
207 snapshot_lock: Arc<AsyncMutex<()>>,
208}
209
210impl CloudFormationService {
211 pub fn new(state: SharedCloudFormationState, deps: CloudFormationDeps) -> Self {
212 Self {
213 state,
214 deps,
215 snapshot_store: None,
216 snapshot_lock: Arc::new(AsyncMutex::new(())),
217 }
218 }
219
220 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
221 self.snapshot_store = Some(store);
222 self
223 }
224
225 async fn save_snapshot(&self) {
226 let Some(store) = self.snapshot_store.clone() else {
227 return;
228 };
229 let _guard = self.snapshot_lock.lock().await;
230 let snapshot = CloudFormationSnapshot {
231 schema_version: CLOUDFORMATION_SNAPSHOT_SCHEMA_VERSION,
232 state: None,
233 accounts: Some(self.state.read().clone()),
234 };
235 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
236 let bytes = serde_json::to_vec(&snapshot)
237 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
238 store.save(&bytes)
239 })
240 .await;
241 match join {
242 Ok(Ok(())) => {}
243 Ok(Err(err)) => tracing::error!(%err, "failed to write cloudformation snapshot"),
244 Err(err) => tracing::error!(%err, "cloudformation snapshot task panicked"),
245 }
246 }
247
248 pub(crate) fn provisioner(
249 &self,
250 stack_id: &str,
251 account_id: &str,
252 region: &str,
253 ) -> ResourceProvisioner {
254 ResourceProvisioner {
255 sqs_state: self.deps.sqs.clone(),
256 sns_state: self.deps.sns.clone(),
257 ssm_state: self.deps.ssm.clone(),
258 iam_state: self.deps.iam.clone(),
259 s3_state: self.deps.s3.clone(),
260 eventbridge_state: self.deps.eventbridge.clone(),
261 dynamodb_state: self.deps.dynamodb.clone(),
262 logs_state: self.deps.logs.clone(),
263 lambda_state: self.deps.lambda.clone(),
264 secretsmanager_state: self.deps.secretsmanager.clone(),
265 kinesis_state: self.deps.kinesis.clone(),
266 kms_state: self.deps.kms.clone(),
267 ecr_state: self.deps.ecr.clone(),
268 cloudwatch_state: self.deps.cloudwatch.clone(),
269 elbv2_state: self.deps.elbv2.clone(),
270 organizations_state: self.deps.organizations.clone(),
271 cognito_state: self.deps.cognito.clone(),
272 rds_state: self.deps.rds.clone(),
273 ecs_state: self.deps.ecs.clone(),
274 acm_state: self.deps.acm.clone(),
275 elasticache_state: self.deps.elasticache.clone(),
276 route53_state: self.deps.route53.clone(),
277 cloudfront_state: self.deps.cloudfront.clone(),
278 stepfunctions_state: self.deps.stepfunctions.clone(),
279 wafv2_state: self.deps.wafv2.clone(),
280 apigateway_state: self.deps.apigateway.clone(),
281 apigatewayv2_state: self.deps.apigatewayv2.clone(),
282 ses_state: self.deps.ses.clone(),
283 app_autoscaling_state: self.deps.application_autoscaling.clone(),
284 athena_state: self.deps.athena.clone(),
285 firehose_state: self.deps.firehose.clone(),
286 glue_state: self.deps.glue.clone(),
287 cloudformation_state: self.state.clone(),
288 delivery: self.deps.delivery.clone(),
289 lambda_runtime: self.deps.lambda_runtime.clone(),
290 account_id: account_id.to_string(),
291 region: region.to_string(),
292 stack_id: stack_id.to_string(),
293 }
294 }
295
296 fn get_param(req: &AwsRequest, key: &str) -> Option<String> {
297 if let Some(v) = req.query_params.get(key) {
299 return Some(v.clone());
300 }
301 let body_params = fakecloud_core::protocol::parse_query_body(&req.body);
303 body_params.get(key).cloned()
304 }
305
306 pub(crate) fn get_all_params(req: &AwsRequest) -> BTreeMap<String, String> {
307 let mut params: BTreeMap<String, String> = req.query_params.clone().into_iter().collect();
308 let body_params = fakecloud_core::protocol::parse_query_body(&req.body);
309 for (k, v) in body_params {
310 params.entry(k).or_insert(v);
311 }
312 params
313 }
314
315 pub(crate) fn extract_tags(params: &BTreeMap<String, String>) -> BTreeMap<String, String> {
316 let mut tags = BTreeMap::new();
317 for i in 1.. {
318 let key_param = format!("Tags.member.{i}.Key");
319 let value_param = format!("Tags.member.{i}.Value");
320 match (params.get(&key_param), params.get(&value_param)) {
321 (Some(k), Some(v)) => {
322 tags.insert(k.clone(), v.clone());
323 }
324 _ => break,
325 }
326 }
327 tags
328 }
329
330 pub(crate) fn extract_parameters(
331 params: &BTreeMap<String, String>,
332 ) -> BTreeMap<String, String> {
333 let mut result = BTreeMap::new();
334 for i in 1.. {
335 let key_param = format!("Parameters.member.{i}.ParameterKey");
336 let value_param = format!("Parameters.member.{i}.ParameterValue");
337 match (params.get(&key_param), params.get(&value_param)) {
338 (Some(k), Some(v)) => {
339 result.insert(k.clone(), v.clone());
340 }
341 _ => break,
342 }
343 }
344 result
345 }
346
347 pub(crate) fn extract_notification_arns(params: &BTreeMap<String, String>) -> Vec<String> {
348 let mut arns = Vec::new();
349 for i in 1.. {
350 let key = format!("NotificationARNs.member.{i}");
351 match params.get(&key) {
352 Some(arn) => arns.push(arn.clone()),
353 None => break,
354 }
355 }
356 arns
357 }
358
359 fn send_stack_notification(
360 delivery: &DeliveryBus,
361 notification_arns: &[String],
362 stack_name: &str,
363 stack_id: &str,
364 status: &str,
365 ) {
366 if notification_arns.is_empty() {
367 return;
368 }
369 let message = format!(
370 "StackId='{}'\nTimestamp='{}'\nEventId='{}'\nLogicalResourceId='{}'\nResourceStatus='{}'\nResourceType='AWS::CloudFormation::Stack'\nStackName='{}'",
371 stack_id,
372 chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ"),
373 uuid::Uuid::new_v4(),
374 stack_name,
375 status,
376 stack_name,
377 );
378 for arn in notification_arns {
379 delivery.publish_to_sns(arn, &message, Some("AWS CloudFormation Notification"));
380 }
381 }
382
383 fn collect_account_imports(
388 state: &SharedCloudFormationState,
389 account_id: &str,
390 skip_stack: Option<&str>,
391 ) -> BTreeMap<String, String> {
392 let mut imports = BTreeMap::new();
393 let accounts = state.read();
394 let Some(state) = accounts.get(account_id) else {
395 return imports;
396 };
397 for (name, export) in &state.exports {
398 if matches!(skip_stack, Some(skip) if skip == export.exporting_stack_name) {
399 continue;
400 }
401 imports.insert(name.clone(), export.value.clone());
402 }
403 imports
404 }
405
406 fn validate_import_values(
411 state: &SharedCloudFormationState,
412 account_id: &str,
413 stack_name: &str,
414 template_body: &str,
415 parameters: &BTreeMap<String, String>,
416 ) -> Result<Vec<String>, AwsServiceError> {
417 let value: serde_json::Value = if template_body.trim_start().starts_with('{') {
418 match serde_json::from_str(template_body) {
419 Ok(v) => v,
420 Err(_) => return Ok(Vec::new()),
421 }
422 } else {
423 match serde_yaml::from_str(template_body) {
424 Ok(v) => v,
425 Err(_) => return Ok(Vec::new()),
426 }
427 };
428 let names = template::collect_import_value_names(&value, parameters);
429 let known = Self::collect_account_imports(state, account_id, Some(stack_name));
430 for n in &names {
431 if !known.contains_key(n) {
432 return Err(AwsServiceError::aws_error(
437 StatusCode::BAD_REQUEST,
438 "InsufficientCapabilitiesException",
439 format!("No export named {n} found."),
440 ));
441 }
442 }
443 Ok(names)
444 }
445
446 fn sync_exports_imports(
450 state: &mut CloudFormationState,
451 stack_id: &str,
452 stack_name: &str,
453 outputs: &[state::StackOutput],
454 imported_names: &[String],
455 ) {
456 let stale_exports: Vec<String> = state
458 .exports
459 .iter()
460 .filter(|(_, e)| e.exporting_stack_name == stack_name)
461 .map(|(k, _)| k.clone())
462 .collect();
463 for k in stale_exports {
464 state.exports.remove(&k);
465 }
466 for entries in state.imports.values_mut() {
468 entries.retain(|s| s != stack_name);
469 }
470 state.imports.retain(|_, v| !v.is_empty());
471
472 for o in outputs {
474 if let Some(export) = &o.export_name {
475 state.exports.insert(
476 export.clone(),
477 state::StackExport {
478 value: o.value.clone(),
479 exporting_stack_id: stack_id.to_string(),
480 exporting_stack_name: stack_name.to_string(),
481 },
482 );
483 }
484 }
485 for name in imported_names {
487 let entry = state.imports.entry(name.clone()).or_default();
488 if !entry.iter().any(|s| s == stack_name) {
489 entry.push(stack_name.to_string());
490 }
491 }
492 }
493
494 fn resolve_template_outputs(
499 template_body: &str,
500 parameters: &BTreeMap<String, String>,
501 resources: &[StackResource],
502 state: &SharedCloudFormationState,
503 ) -> Vec<state::StackOutput> {
504 let value: serde_json::Value = if template_body.trim_start().starts_with('{') {
505 match serde_json::from_str(template_body) {
506 Ok(v) => v,
507 Err(_) => return Vec::new(),
508 }
509 } else {
510 match serde_yaml::from_str(template_body) {
511 Ok(v) => v,
512 Err(_) => return Vec::new(),
513 }
514 };
515
516 let resources_obj = match value.get("Resources").and_then(|v| v.as_object()) {
517 Some(o) => o.clone(),
518 None => return Vec::new(),
519 };
520
521 let mut physical_ids: BTreeMap<String, String> = BTreeMap::new();
522 let mut attributes: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
523 for r in resources {
524 physical_ids.insert(r.logical_id.clone(), r.physical_id.clone());
525 attributes.insert(r.logical_id.clone(), r.attributes.clone());
526 }
527
528 let imports = {
529 let accounts = state.read();
530 let mut out = BTreeMap::new();
531 for (_account, st) in accounts.iter() {
534 for (name, export) in &st.exports {
535 out.insert(name.clone(), export.value.clone());
536 }
537 }
538 out
539 };
540
541 let parsed = match template::parse_outputs(
542 &value,
543 parameters,
544 &resources_obj,
545 &physical_ids,
546 &attributes,
547 &imports,
548 ) {
549 Ok(o) => o,
550 Err(_) => return Vec::new(),
551 };
552
553 parsed
554 .into_iter()
555 .map(|o| state::StackOutput {
556 key: o.logical_id,
557 value: o.value,
558 description: o.description,
559 export_name: o.export_name,
560 })
561 .collect()
562 }
563
564 fn ensure_export_uniqueness(
567 state: &SharedCloudFormationState,
568 account_id: &str,
569 stack_name: &str,
570 outputs: &[state::StackOutput],
571 ) -> Result<(), AwsServiceError> {
572 let existing = Self::collect_account_imports(state, account_id, Some(stack_name));
573 for o in outputs {
574 if let Some(export) = &o.export_name {
575 if existing.contains_key(export) {
576 return Err(AwsServiceError::aws_error(
580 StatusCode::BAD_REQUEST,
581 "AlreadyExistsException",
582 format!("Export with name {export} is already exported by another stack"),
583 ));
584 }
585 }
586 }
587 Ok(())
588 }
589
590 fn create_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
591 let params = Self::get_all_params(req);
592
593 let stack_name = params.get("StackName").ok_or_else(|| {
596 AwsServiceError::aws_error(
597 StatusCode::BAD_REQUEST,
598 "ValidationError",
599 "StackName is required",
600 )
601 })?;
602
603 let empty = String::new();
607 let template_body = params.get("TemplateBody").unwrap_or(&empty);
608
609 {
611 let accounts = self.state.read();
612 let empty = CloudFormationState::new(&req.account_id, &req.region);
613 let state = accounts.get(&req.account_id).unwrap_or(&empty);
614 if let Some(existing) = state.stacks.get(stack_name.as_str()) {
615 if existing.status != "DELETE_COMPLETE" {
616 return Err(AwsServiceError::aws_error(
617 StatusCode::BAD_REQUEST,
618 "AlreadyExistsException",
619 format!("Stack [{stack_name}] already exists"),
620 ));
621 }
622 }
623 }
624
625 let tags = Self::extract_tags(¶ms);
626 let mut parameters = Self::extract_parameters(¶ms);
627 let notification_arns = Self::extract_notification_arns(¶ms);
628
629 let stack_id = format!(
632 "arn:aws:cloudformation:{}:{}:stack/{}/{}",
633 req.region,
634 req.account_id,
635 stack_name,
636 uuid::Uuid::new_v4()
637 );
638 parameters
639 .entry("AWS::Region".to_string())
640 .or_insert_with(|| req.region.clone());
641 parameters
642 .entry("AWS::AccountId".to_string())
643 .or_insert_with(|| req.account_id.clone());
644 parameters
645 .entry("AWS::StackId".to_string())
646 .or_insert_with(|| stack_id.clone());
647 parameters
648 .entry("AWS::StackName".to_string())
649 .or_insert_with(|| stack_name.clone());
650 parameters
651 .entry("AWS::Partition".to_string())
652 .or_insert_with(|| template::partition_for_region(&req.region).to_string());
653 parameters
654 .entry("AWS::URLSuffix".to_string())
655 .or_insert_with(|| template::url_suffix_for_region(&req.region).to_string());
656 parameters.insert(
660 "AWS::NotificationARNs".to_string(),
661 serde_json::to_string(¬ification_arns).unwrap_or_else(|_| "[]".to_string()),
662 );
663
664 let parsed = template::parse_template(template_body, ¶meters).unwrap_or_else(|_| {
669 template::ParsedTemplate {
670 description: None,
671 resources: Vec::new(),
672 outputs: Vec::new(),
673 }
674 });
675
676 let imported_names = Self::validate_import_values(
680 &self.state,
681 &req.account_id,
682 stack_name,
683 template_body,
684 ¶meters,
685 )?;
686
687 let provisioner = self.provisioner(&stack_id, &req.account_id, &req.region);
688 let resources = {
697 let provision = || {
698 provision_stack_resources(
699 &provisioner,
700 &parsed.resources,
701 template_body,
702 ¶meters,
703 )
704 };
705 match tokio::runtime::Handle::try_current().map(|h| h.runtime_flavor()) {
706 Ok(tokio::runtime::RuntimeFlavor::MultiThread) => {
707 tokio::task::block_in_place(provision)
708 }
709 _ => provision(),
710 }
711 }?;
712
713 let outputs =
714 Self::resolve_template_outputs(template_body, ¶meters, &resources, &self.state);
715
716 Self::ensure_export_uniqueness(&self.state, &req.account_id, stack_name, &outputs)?;
717
718 let stack = Stack {
719 name: stack_name.clone(),
720 stack_id: stack_id.clone(),
721 template: template_body.clone(),
722 status: "CREATE_COMPLETE".to_string(),
723 resources: resources.clone(),
724 parameters,
725 tags,
726 created_at: Utc::now(),
727 updated_at: None,
728 description: parsed.description,
729 notification_arns: notification_arns.clone(),
730 outputs: outputs.clone(),
731 };
732
733 {
734 let mut accounts = self.state.write();
735 let state = accounts.get_or_create(&req.account_id);
736 state.stacks.insert(stack_name.clone(), stack);
737 Self::sync_exports_imports(state, &stack_id, stack_name, &outputs, &imported_names);
738
739 record_stack_status_event(
741 state,
742 &stack_id,
743 stack_name,
744 "AWS::CloudFormation::Stack",
745 "CREATE_IN_PROGRESS",
746 );
747 let changes: Vec<ResourceChange> = resources
748 .iter()
749 .map(|r| ResourceChange {
750 action: ResourceChangeAction::Create,
751 logical_id: r.logical_id.clone(),
752 physical_id: r.physical_id.clone(),
753 resource_type: r.resource_type.clone(),
754 })
755 .collect();
756 record_stack_events(state, &stack_id, stack_name, &changes);
757 record_stack_status_event(
758 state,
759 &stack_id,
760 stack_name,
761 "AWS::CloudFormation::Stack",
762 "CREATE_COMPLETE",
763 );
764 }
765
766 Self::send_stack_notification(
767 &self.deps.delivery,
768 ¬ification_arns,
769 stack_name,
770 &stack_id,
771 "CREATE_COMPLETE",
772 );
773
774 Ok(AwsResponse::xml(
775 StatusCode::OK,
776 xml_responses::create_stack_response(&stack_id, &req.request_id),
777 ))
778 }
779
780 fn delete_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
781 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
782 AwsServiceError::aws_error(
783 StatusCode::BAD_REQUEST,
784 "ValidationError",
785 "StackName is required",
786 )
787 })?;
788
789 let mut accounts = self.state.write();
790 let state = accounts.get_or_create(&req.account_id);
791
792 let stack = state.stacks.values_mut().find(|s| {
794 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
795 });
796
797 if let Some(stack) = stack {
798 let stack_id = stack.stack_id.clone();
799 let stack_name_for_notif = stack.name.clone();
800 let notification_arns = stack.notification_arns.clone();
801 let resources: Vec<_> = stack.resources.clone();
802
803 let owned_exports: Vec<String> = state
806 .exports
807 .iter()
808 .filter(|(_, e)| e.exporting_stack_name == stack_name_for_notif)
809 .map(|(k, _)| k.clone())
810 .collect();
811 for export in &owned_exports {
812 if let Some(consumers) = state.imports.get(export) {
813 let consumers: Vec<&String> = consumers
814 .iter()
815 .filter(|c| **c != stack_name_for_notif)
816 .collect();
817 if !consumers.is_empty() {
818 let names: Vec<&str> = consumers.iter().map(|s| s.as_str()).collect();
819 return Err(AwsServiceError::aws_error(
826 StatusCode::BAD_REQUEST,
827 "TokenAlreadyExistsException",
828 format!(
829 "Export {export} cannot be deleted as it is in use by {}",
830 names.join(", ")
831 ),
832 ));
833 }
834 }
835 }
836
837 drop(accounts);
840 let provisioner = self.provisioner(&stack_id, &req.account_id, &req.region);
841
842 for resource in resources.iter().rev() {
844 let _ = provisioner.delete_resource(resource);
845 }
846
847 let mut accounts = self.state.write();
849 let state = accounts.get_or_create(&req.account_id);
850 if let Some(stack) = state.stacks.values_mut().find(|s| s.stack_id == stack_id) {
851 stack.status = "DELETE_COMPLETE".to_string();
852 stack.resources.clear();
853 stack.outputs.clear();
854 }
855 let stale_exports: Vec<String> = state
857 .exports
858 .iter()
859 .filter(|(_, e)| e.exporting_stack_name == stack_name_for_notif)
860 .map(|(k, _)| k.clone())
861 .collect();
862 for k in stale_exports {
863 state.exports.remove(&k);
864 }
865 for entries in state.imports.values_mut() {
866 entries.retain(|s| s != &stack_name_for_notif);
867 }
868 state.imports.retain(|_, v| !v.is_empty());
869 drop(accounts);
870
871 Self::send_stack_notification(
872 &self.deps.delivery,
873 ¬ification_arns,
874 &stack_name_for_notif,
875 &stack_id,
876 "DELETE_COMPLETE",
877 );
878 }
879
880 Ok(AwsResponse::xml(
881 StatusCode::OK,
882 xml_responses::delete_stack_response(&req.request_id),
883 ))
884 }
885
886 fn describe_stacks(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
887 let stack_name = Self::get_param(req, "StackName");
888
889 let accounts = self.state.read();
890 let empty = CloudFormationState::new(&req.account_id, &req.region);
891 let state = accounts.get(&req.account_id).unwrap_or(&empty);
892 let stacks: Vec<Stack> = if let Some(ref name) = stack_name {
893 state
894 .stacks
895 .values()
896 .filter(|s| {
897 (s.name == *name || s.stack_id == *name) && s.status != "DELETE_COMPLETE"
898 })
899 .cloned()
900 .collect()
901 } else {
902 state
903 .stacks
904 .values()
905 .filter(|s| s.status != "DELETE_COMPLETE")
906 .cloned()
907 .collect()
908 };
909
910 if let Some(ref name) = stack_name {
921 if stacks.is_empty() {
922 return Err(AwsServiceError::aws_error(
923 StatusCode::BAD_REQUEST,
924 "ValidationError",
925 format!("Stack with id {name} does not exist"),
926 ));
927 }
928 }
929
930 Ok(AwsResponse::xml(
931 StatusCode::OK,
932 xml_responses::describe_stacks_response(&stacks, &req.request_id),
933 ))
934 }
935
936 fn list_stacks(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
937 let accounts = self.state.read();
938 let empty = CloudFormationState::new(&req.account_id, &req.region);
939 let state = accounts.get(&req.account_id).unwrap_or(&empty);
940 let stacks: Vec<Stack> = state.stacks.values().cloned().collect();
941
942 Ok(AwsResponse::xml(
943 StatusCode::OK,
944 xml_responses::list_stacks_response(&stacks, &req.request_id),
945 ))
946 }
947
948 fn list_stack_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
949 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
955 AwsServiceError::aws_error(
956 StatusCode::BAD_REQUEST,
957 "ValidationError",
958 "StackName is required",
959 )
960 })?;
961
962 let accounts = self.state.read();
963 let empty = CloudFormationState::new(&req.account_id, &req.region);
964 let state = accounts.get(&req.account_id).unwrap_or(&empty);
965 let resources = state
966 .stacks
967 .values()
968 .find(|s| {
969 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
970 })
971 .map(|s| s.resources.clone())
972 .unwrap_or_default();
973
974 Ok(AwsResponse::xml(
975 StatusCode::OK,
976 xml_responses::list_stack_resources_response(&resources, &req.request_id),
977 ))
978 }
979
980 fn describe_stack_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
981 let stack_name = Self::get_param(req, "StackName").unwrap_or_default();
984
985 let accounts = self.state.read();
986 let empty = CloudFormationState::new(&req.account_id, &req.region);
987 let state = accounts.get(&req.account_id).unwrap_or(&empty);
988 let (resources, resolved_name) = state
989 .stacks
990 .values()
991 .find(|s| {
992 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
993 })
994 .map(|s| (s.resources.clone(), s.name.clone()))
995 .unwrap_or_else(|| (Vec::new(), stack_name.clone()));
996
997 Ok(AwsResponse::xml(
998 StatusCode::OK,
999 xml_responses::describe_stack_resources_response(
1000 &resources,
1001 &resolved_name,
1002 &req.request_id,
1003 ),
1004 ))
1005 }
1006
1007 fn update_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1008 let mut input = UpdateStackInput::from_params(req)?;
1009
1010 let found_stack_id = {
1012 let accounts = self.state.read();
1013 let empty = CloudFormationState::new(&req.account_id, &req.region);
1014 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1015 state
1016 .stacks
1017 .values()
1018 .find(|s| {
1019 (s.name == input.stack_name || s.stack_id == input.stack_name)
1020 && s.status != "DELETE_COMPLETE"
1021 })
1022 .map(|s| s.stack_id.clone())
1023 .unwrap_or_default()
1024 };
1025
1026 input
1030 .parameters
1031 .entry("AWS::Region".to_string())
1032 .or_insert_with(|| req.region.clone());
1033 input
1034 .parameters
1035 .entry("AWS::AccountId".to_string())
1036 .or_insert_with(|| req.account_id.clone());
1037 input
1038 .parameters
1039 .entry("AWS::StackId".to_string())
1040 .or_insert_with(|| found_stack_id.clone());
1041 input
1042 .parameters
1043 .entry("AWS::StackName".to_string())
1044 .or_insert_with(|| input.stack_name.clone());
1045 input
1046 .parameters
1047 .entry("AWS::Partition".to_string())
1048 .or_insert_with(|| template::partition_for_region(&req.region).to_string());
1049 input
1050 .parameters
1051 .entry("AWS::URLSuffix".to_string())
1052 .or_insert_with(|| template::url_suffix_for_region(&req.region).to_string());
1053 if !input.notification_arns.is_empty() {
1058 input.parameters.insert(
1059 "AWS::NotificationARNs".to_string(),
1060 serde_json::to_string(&input.notification_arns)
1061 .unwrap_or_else(|_| "[]".to_string()),
1062 );
1063 } else {
1064 let existing: Vec<String> = {
1067 let accounts = self.state.read();
1068 accounts
1069 .get(&req.account_id)
1070 .and_then(|s| {
1071 s.stacks
1072 .values()
1073 .find(|st| st.stack_id == found_stack_id)
1074 .map(|st| st.notification_arns.clone())
1075 })
1076 .unwrap_or_default()
1077 };
1078 input.parameters.insert(
1079 "AWS::NotificationARNs".to_string(),
1080 serde_json::to_string(&existing).unwrap_or_else(|_| "[]".to_string()),
1081 );
1082 }
1083
1084 let parsed = template::parse_template(&input.template_body, &input.parameters)
1089 .unwrap_or_else(|_| template::ParsedTemplate {
1090 description: None,
1091 resources: Vec::new(),
1092 outputs: Vec::new(),
1093 });
1094
1095 let imported_names = Self::validate_import_values(
1096 &self.state,
1097 &req.account_id,
1098 &input.stack_name,
1099 &input.template_body,
1100 &input.parameters,
1101 )?;
1102
1103 let provisioner = self.provisioner(&found_stack_id, &req.account_id, &req.region);
1104
1105 let mut accounts = self.state.write();
1106 let state = accounts.get_or_create(&req.account_id);
1107 let stack_exists = state.stacks.values().any(|s| {
1116 (s.name == input.stack_name || s.stack_id == input.stack_name)
1117 && s.status != "DELETE_COMPLETE"
1118 });
1119 if !stack_exists {
1120 let stack_id = if found_stack_id.is_empty() {
1121 format!(
1122 "arn:aws:cloudformation:{}:{}:stack/{}/{}",
1123 req.region,
1124 req.account_id,
1125 input.stack_name,
1126 uuid::Uuid::new_v4()
1127 )
1128 } else {
1129 found_stack_id.clone()
1130 };
1131 return Ok(AwsResponse::xml(
1132 StatusCode::OK,
1133 xml_responses::update_stack_response(&stack_id, &req.request_id),
1134 ));
1135 }
1136 let (update_result, stack_id, stack_name_owned, resources_snapshot, notification_arns) = {
1137 let stack = state
1138 .stacks
1139 .values_mut()
1140 .find(|s| {
1141 (s.name == input.stack_name || s.stack_id == input.stack_name)
1142 && s.status != "DELETE_COMPLETE"
1143 })
1144 .expect("stack existence checked above");
1145
1146 stack.status = "UPDATE_IN_PROGRESS".to_string();
1147 let update_result = apply_resource_updates(
1148 stack,
1149 &parsed.resources,
1150 &input.template_body,
1151 &input.parameters,
1152 &provisioner,
1153 );
1154
1155 let stack_id = stack.stack_id.clone();
1156 let stack_name_owned = stack.name.clone();
1157 stack.template = input.template_body.clone();
1158 stack.status = if update_result.is_err() {
1159 "UPDATE_ROLLBACK_COMPLETE".to_string()
1160 } else {
1161 "UPDATE_COMPLETE".to_string()
1162 };
1163 stack.parameters = input.parameters.clone();
1164 if !input.tags.is_empty() {
1165 stack.tags = input.tags;
1166 }
1167 stack.updated_at = Some(Utc::now());
1168 stack.description = parsed.description;
1169 if !input.notification_arns.is_empty() {
1170 stack.notification_arns = input.notification_arns.clone();
1171 }
1172 if update_result.is_ok() {
1173 stack.outputs.clear();
1174 }
1175 (
1176 update_result,
1177 stack_id,
1178 stack_name_owned,
1179 stack.resources.clone(),
1180 stack.notification_arns.clone(),
1181 )
1182 };
1183
1184 record_stack_status_event(
1186 state,
1187 &stack_id,
1188 &stack_name_owned,
1189 "AWS::CloudFormation::Stack",
1190 "UPDATE_IN_PROGRESS",
1191 );
1192 let update_result = match update_result {
1193 Ok(changes) => {
1194 record_stack_events(state, &stack_id, &stack_name_owned, &changes);
1195 record_stack_status_event(
1196 state,
1197 &stack_id,
1198 &stack_name_owned,
1199 "AWS::CloudFormation::Stack",
1200 "UPDATE_COMPLETE",
1201 );
1202 Ok(())
1203 }
1204 Err(e) => {
1205 record_stack_status_event(
1206 state,
1207 &stack_id,
1208 &stack_name_owned,
1209 "AWS::CloudFormation::Stack",
1210 "UPDATE_ROLLBACK_COMPLETE",
1211 );
1212 Err(e)
1213 }
1214 };
1215 let stack_name_for_notif = stack_name_owned.clone();
1216
1217 if let Err(error_msg) = update_result {
1218 drop(accounts);
1219 Self::send_stack_notification(
1220 &self.deps.delivery,
1221 ¬ification_arns,
1222 &stack_name_for_notif,
1223 &stack_id,
1224 "UPDATE_FAILED",
1225 );
1226 return Err(AwsServiceError::aws_error(
1227 StatusCode::BAD_REQUEST,
1228 "InsufficientCapabilitiesException",
1229 error_msg,
1230 ));
1231 }
1232
1233 drop(accounts);
1234
1235 let outputs = Self::resolve_template_outputs(
1236 &input.template_body,
1237 &input.parameters,
1238 &resources_snapshot,
1239 &self.state,
1240 );
1241 Self::ensure_export_uniqueness(&self.state, &req.account_id, &input.stack_name, &outputs)?;
1242 {
1243 let mut accounts = self.state.write();
1244 let state = accounts.get_or_create(&req.account_id);
1245 if let Some(stack) = state
1246 .stacks
1247 .values_mut()
1248 .find(|s| s.stack_id == stack_id && s.status != "DELETE_COMPLETE")
1249 {
1250 stack.outputs = outputs.clone();
1251 }
1252 Self::sync_exports_imports(
1253 state,
1254 &stack_id,
1255 &input.stack_name,
1256 &outputs,
1257 &imported_names,
1258 );
1259 }
1260
1261 Self::send_stack_notification(
1262 &self.deps.delivery,
1263 ¬ification_arns,
1264 &stack_name_for_notif,
1265 &stack_id,
1266 "UPDATE_COMPLETE",
1267 );
1268
1269 Ok(AwsResponse::xml(
1270 StatusCode::OK,
1271 xml_responses::update_stack_response(&stack_id, &req.request_id),
1272 ))
1273 }
1274
1275 fn get_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1276 let stack_name = Self::get_param(req, "StackName").unwrap_or_default();
1278
1279 let accounts = self.state.read();
1280 let empty = CloudFormationState::new(&req.account_id, &req.region);
1281 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1282 let body = state
1287 .stacks
1288 .values()
1289 .find(|s| {
1290 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
1291 })
1292 .map(|s| s.template.clone())
1293 .unwrap_or_default();
1294
1295 Ok(AwsResponse::xml(
1296 StatusCode::OK,
1297 xml_responses::get_template_response(&body, &req.request_id),
1298 ))
1299 }
1300}
1301
1302#[async_trait]
1303impl AwsService for CloudFormationService {
1304 fn service_name(&self) -> &str {
1305 "cloudformation"
1306 }
1307
1308 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1309 let action = req.action.as_str();
1310
1311 crate::input_constraints::validate_input(action, &Self::get_all_params(&req))?;
1318
1319 let mutates = matches!(
1323 action,
1324 "CreateStack"
1325 | "DeleteStack"
1326 | "UpdateStack"
1327 | "CreateChangeSet"
1328 | "DeleteChangeSet"
1329 | "ExecuteChangeSet"
1330 | "CreateStackSet"
1331 | "DeleteStackSet"
1332 | "CreateStackRefactor"
1333 | "CreateGeneratedTemplate"
1334 | "DeleteGeneratedTemplate"
1335 | "SetStackPolicy"
1336 | "UpdateTerminationProtection"
1337 | "ActivateOrganizationsAccess"
1338 | "DeactivateOrganizationsAccess"
1339 );
1340 let result = match action {
1341 "CreateStack" => self.create_stack(&req),
1342 "DeleteStack" => self.delete_stack(&req),
1343 "DescribeStacks" => self.describe_stacks(&req),
1344 "ListStacks" => self.list_stacks(&req),
1345 "ListStackResources" => self.list_stack_resources(&req),
1346 "DescribeStackResources" => self.describe_stack_resources(&req),
1347 "UpdateStack" => self.update_stack(&req),
1348 "GetTemplate" => self.get_template(&req),
1349 _ => self.handle_extra_action(&req),
1350 };
1351 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
1352 self.save_snapshot().await;
1353 }
1354 result
1355 }
1356
1357 fn supported_actions(&self) -> &[&str] {
1358 &[
1359 "ActivateOrganizationsAccess",
1360 "ActivateType",
1361 "BatchDescribeTypeConfigurations",
1362 "CancelUpdateStack",
1363 "ContinueUpdateRollback",
1364 "CreateChangeSet",
1365 "CreateGeneratedTemplate",
1366 "CreateStack",
1367 "CreateStackInstances",
1368 "CreateStackRefactor",
1369 "CreateStackSet",
1370 "DeactivateOrganizationsAccess",
1371 "DeactivateType",
1372 "DeleteChangeSet",
1373 "DeleteGeneratedTemplate",
1374 "DeleteStack",
1375 "DeleteStackInstances",
1376 "DeleteStackSet",
1377 "DeregisterType",
1378 "DescribeAccountLimits",
1379 "DescribeChangeSet",
1380 "DescribeChangeSetHooks",
1381 "DescribeEvents",
1382 "DescribeGeneratedTemplate",
1383 "DescribeOrganizationsAccess",
1384 "DescribePublisher",
1385 "DescribeResourceScan",
1386 "DescribeStackDriftDetectionStatus",
1387 "DescribeStackEvents",
1388 "DescribeStackInstance",
1389 "DescribeStackRefactor",
1390 "DescribeStackResource",
1391 "DescribeStackResourceDrifts",
1392 "DescribeStackResources",
1393 "DescribeStackSet",
1394 "DescribeStackSetOperation",
1395 "DescribeStacks",
1396 "DescribeType",
1397 "DescribeTypeRegistration",
1398 "DetectStackDrift",
1399 "DetectStackResourceDrift",
1400 "DetectStackSetDrift",
1401 "EstimateTemplateCost",
1402 "ExecuteChangeSet",
1403 "ExecuteStackRefactor",
1404 "GetGeneratedTemplate",
1405 "GetHookResult",
1406 "GetStackPolicy",
1407 "GetTemplate",
1408 "GetTemplateSummary",
1409 "ImportStacksToStackSet",
1410 "ListChangeSets",
1411 "ListExports",
1412 "ListGeneratedTemplates",
1413 "ListHookResults",
1414 "ListImports",
1415 "ListResourceScanRelatedResources",
1416 "ListResourceScanResources",
1417 "ListResourceScans",
1418 "ListStackInstanceResourceDrifts",
1419 "ListStackInstances",
1420 "ListStackRefactorActions",
1421 "ListStackRefactors",
1422 "ListStackResources",
1423 "ListStackSetAutoDeploymentTargets",
1424 "ListStackSetOperationResults",
1425 "ListStackSetOperations",
1426 "ListStackSets",
1427 "ListStacks",
1428 "ListTypeRegistrations",
1429 "ListTypeVersions",
1430 "ListTypes",
1431 "PublishType",
1432 "RecordHandlerProgress",
1433 "RegisterPublisher",
1434 "RegisterType",
1435 "RollbackStack",
1436 "SetStackPolicy",
1437 "SetTypeConfiguration",
1438 "SetTypeDefaultVersion",
1439 "SignalResource",
1440 "StartResourceScan",
1441 "StopStackSetOperation",
1442 "TestType",
1443 "UpdateGeneratedTemplate",
1444 "UpdateStack",
1445 "UpdateStackInstances",
1446 "UpdateStackSet",
1447 "UpdateTerminationProtection",
1448 "ValidateTemplate",
1449 ]
1450 }
1451}
1452
1453struct UpdateStackInput {
1455 stack_name: String,
1456 template_body: String,
1457 parameters: BTreeMap<String, String>,
1458 tags: BTreeMap<String, String>,
1459 notification_arns: Vec<String>,
1460}
1461
1462impl UpdateStackInput {
1463 fn from_params(req: &AwsRequest) -> Result<Self, AwsServiceError> {
1464 let params = CloudFormationService::get_all_params(req);
1465
1466 let stack_name = params
1467 .get("StackName")
1468 .ok_or_else(|| {
1469 AwsServiceError::aws_error(
1470 StatusCode::BAD_REQUEST,
1471 "ValidationError",
1472 "StackName is required",
1473 )
1474 })?
1475 .to_string();
1476
1477 let template_body = params.get("TemplateBody").cloned().unwrap_or_default();
1482
1483 Ok(Self {
1484 stack_name,
1485 template_body,
1486 parameters: CloudFormationService::extract_parameters(¶ms),
1487 tags: CloudFormationService::extract_tags(¶ms),
1488 notification_arns: CloudFormationService::extract_notification_arns(¶ms),
1489 })
1490 }
1491}
1492
1493#[derive(Debug, Clone)]
1497pub(crate) struct ResourceChange {
1498 pub action: ResourceChangeAction,
1499 pub logical_id: String,
1500 pub physical_id: String,
1501 pub resource_type: String,
1502}
1503
1504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1505pub(crate) enum ResourceChangeAction {
1506 Create,
1507 Update,
1508 Delete,
1509}
1510
1511impl ResourceChangeAction {
1512 pub fn status_in_progress(self) -> &'static str {
1513 match self {
1514 Self::Create => "CREATE_IN_PROGRESS",
1515 Self::Update => "UPDATE_IN_PROGRESS",
1516 Self::Delete => "DELETE_IN_PROGRESS",
1517 }
1518 }
1519 pub fn status_complete(self) -> &'static str {
1520 match self {
1521 Self::Create => "CREATE_COMPLETE",
1522 Self::Update => "UPDATE_COMPLETE",
1523 Self::Delete => "DELETE_COMPLETE",
1524 }
1525 }
1526}
1527
1528pub(crate) fn apply_resource_updates(
1533 stack: &mut crate::state::Stack,
1534 new_resource_defs: &[template::ResourceDefinition],
1535 template_body: &str,
1536 parameters: &BTreeMap<String, String>,
1537 provisioner: &crate::resource_provisioner::ResourceProvisioner,
1538) -> Result<Vec<ResourceChange>, String> {
1539 let mut changes: Vec<ResourceChange> = Vec::new();
1540 let old_logical_ids: std::collections::HashSet<String> = stack
1541 .resources
1542 .iter()
1543 .map(|r| r.logical_id.clone())
1544 .collect();
1545 let new_logical_ids: std::collections::HashSet<String> = new_resource_defs
1546 .iter()
1547 .map(|r| r.logical_id.clone())
1548 .collect();
1549
1550 let to_remove: Vec<_> = stack
1552 .resources
1553 .iter()
1554 .filter(|r| !new_logical_ids.contains(&r.logical_id))
1555 .cloned()
1556 .collect();
1557 for resource in &to_remove {
1558 let _ = provisioner.delete_resource(resource);
1559 changes.push(ResourceChange {
1560 action: ResourceChangeAction::Delete,
1561 logical_id: resource.logical_id.clone(),
1562 physical_id: resource.physical_id.clone(),
1563 resource_type: resource.resource_type.clone(),
1564 });
1565 }
1566 stack
1567 .resources
1568 .retain(|r| new_logical_ids.contains(&r.logical_id));
1569
1570 let mut physical_ids: BTreeMap<String, String> = stack
1572 .resources
1573 .iter()
1574 .map(|r| (r.logical_id.clone(), r.physical_id.clone()))
1575 .collect();
1576 let mut attributes: BTreeMap<String, BTreeMap<String, String>> = stack
1577 .resources
1578 .iter()
1579 .map(|r| (r.logical_id.clone(), r.attributes.clone()))
1580 .collect();
1581
1582 for resource_def in new_resource_defs {
1584 let resolved_def = template::resolve_resource_properties_with_attrs(
1585 resource_def,
1586 template_body,
1587 parameters,
1588 &physical_ids,
1589 &attributes,
1590 )
1591 .map_err(|e| {
1592 format!(
1593 "Failed to resolve resource {}: {e}",
1594 resource_def.logical_id
1595 )
1596 })?;
1597
1598 if !old_logical_ids.contains(&resource_def.logical_id) {
1599 match provisioner.create_resource(&resolved_def) {
1600 Ok(stack_resource) => {
1601 changes.push(ResourceChange {
1602 action: ResourceChangeAction::Create,
1603 logical_id: stack_resource.logical_id.clone(),
1604 physical_id: stack_resource.physical_id.clone(),
1605 resource_type: stack_resource.resource_type.clone(),
1606 });
1607 physical_ids.insert(
1608 stack_resource.logical_id.clone(),
1609 stack_resource.physical_id.clone(),
1610 );
1611 attributes.insert(
1612 stack_resource.logical_id.clone(),
1613 stack_resource.attributes.clone(),
1614 );
1615 stack.resources.push(stack_resource);
1616 }
1617 Err(e) => {
1618 tracing::warn!(
1619 "Failed to create resource {} during update: {e}",
1620 resource_def.logical_id
1621 );
1622 return Err(format!(
1623 "Failed to create resource {}: {e}",
1624 resource_def.logical_id
1625 ));
1626 }
1627 }
1628 } else {
1629 let existing = stack
1635 .resources
1636 .iter()
1637 .find(|r| r.logical_id == resource_def.logical_id)
1638 .cloned();
1639 if let Some(existing) = existing {
1640 match provisioner.update_resource(&existing, &resolved_def) {
1641 Ok(Some(updated)) => {
1642 changes.push(ResourceChange {
1643 action: ResourceChangeAction::Update,
1644 logical_id: updated.logical_id.clone(),
1645 physical_id: updated.physical_id.clone(),
1646 resource_type: updated.resource_type.clone(),
1647 });
1648 physical_ids
1649 .insert(updated.logical_id.clone(), updated.physical_id.clone());
1650 attributes.insert(updated.logical_id.clone(), updated.attributes.clone());
1651 if let Some(slot) = stack
1652 .resources
1653 .iter_mut()
1654 .find(|r| r.logical_id == updated.logical_id)
1655 {
1656 *slot = updated;
1657 }
1658 }
1659 Ok(None) => {
1660 }
1663 Err(e) => {
1664 tracing::warn!(
1665 "Failed to update resource {} during update: {e}",
1666 resource_def.logical_id
1667 );
1668 return Err(format!(
1669 "Failed to update resource {}: {e}",
1670 resource_def.logical_id
1671 ));
1672 }
1673 }
1674 }
1675 }
1676 }
1677
1678 Ok(changes)
1679}
1680
1681pub(crate) fn record_event(
1685 state: &mut crate::state::CloudFormationState,
1686 stack_id: &str,
1687 stack_name: &str,
1688 logical_id: &str,
1689 physical_id: &str,
1690 resource_type: &str,
1691 status: &str,
1692) {
1693 use serde_json::json;
1694 let event_id = format!(
1695 "{}-{:x}",
1696 logical_id,
1697 std::time::SystemTime::now()
1698 .duration_since(std::time::UNIX_EPOCH)
1699 .map(|d| d.as_nanos())
1700 .unwrap_or(0)
1701 );
1702 let entry = json!({
1703 "EventId": event_id,
1704 "StackId": stack_id,
1705 "StackName": stack_name,
1706 "LogicalResourceId": logical_id,
1707 "PhysicalResourceId": physical_id,
1708 "ResourceType": resource_type,
1709 "ResourceStatus": status,
1710 "Timestamp": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1711 });
1712 state
1713 .events
1714 .entry(stack_id.to_string())
1715 .or_default()
1716 .push(entry);
1717}
1718
1719pub(crate) fn record_stack_events(
1723 state: &mut crate::state::CloudFormationState,
1724 stack_id: &str,
1725 stack_name: &str,
1726 changes: &[ResourceChange],
1727) {
1728 for ch in changes {
1729 record_event(
1730 state,
1731 stack_id,
1732 stack_name,
1733 &ch.logical_id,
1734 &ch.physical_id,
1735 &ch.resource_type,
1736 ch.action.status_in_progress(),
1737 );
1738 record_event(
1739 state,
1740 stack_id,
1741 stack_name,
1742 &ch.logical_id,
1743 &ch.physical_id,
1744 &ch.resource_type,
1745 ch.action.status_complete(),
1746 );
1747 }
1748}
1749
1750pub(crate) fn record_stack_status_event(
1754 state: &mut crate::state::CloudFormationState,
1755 stack_id: &str,
1756 stack_name: &str,
1757 resource_type: &str,
1758 status: &str,
1759) {
1760 record_event(
1761 state,
1762 stack_id,
1763 stack_name,
1764 stack_name,
1765 stack_id,
1766 resource_type,
1767 status,
1768 );
1769}
1770
1771#[cfg(test)]
1772mod tests {
1773 use super::*;
1774 use http::HeaderMap;
1775 use parking_lot::RwLock;
1776 use std::collections::HashMap;
1777 use std::sync::Arc;
1778
1779 fn make_service() -> CloudFormationService {
1780 let cf_state = Arc::new(RwLock::new(
1781 fakecloud_core::multi_account::MultiAccountState::new(
1782 "123456789012",
1783 "us-east-1",
1784 "http://localhost:4566",
1785 ),
1786 ));
1787 let deps = CloudFormationDeps {
1788 sqs: Arc::new(RwLock::new(
1789 fakecloud_core::multi_account::MultiAccountState::new(
1790 "123456789012",
1791 "us-east-1",
1792 "http://localhost:4566",
1793 ),
1794 )),
1795 sns: Arc::new(RwLock::new(
1796 fakecloud_core::multi_account::MultiAccountState::new(
1797 "123456789012",
1798 "us-east-1",
1799 "http://localhost:4566",
1800 ),
1801 )),
1802 ssm: Arc::new(RwLock::new(
1803 fakecloud_core::multi_account::MultiAccountState::new(
1804 "123456789012",
1805 "us-east-1",
1806 "http://localhost:4566",
1807 ),
1808 )),
1809 iam: Arc::new(RwLock::new(
1810 fakecloud_core::multi_account::MultiAccountState::new(
1811 "123456789012",
1812 "us-east-1",
1813 "",
1814 ),
1815 )),
1816 s3: Arc::new(RwLock::new(
1817 fakecloud_core::multi_account::MultiAccountState::new(
1818 "123456789012",
1819 "us-east-1",
1820 "",
1821 ),
1822 )),
1823 eventbridge: Arc::new(RwLock::new(
1824 fakecloud_core::multi_account::MultiAccountState::new(
1825 "123456789012",
1826 "us-east-1",
1827 "",
1828 ),
1829 )),
1830 dynamodb: Arc::new(RwLock::new(
1831 fakecloud_core::multi_account::MultiAccountState::new(
1832 "123456789012",
1833 "us-east-1",
1834 "",
1835 ),
1836 )),
1837 logs: Arc::new(RwLock::new(
1838 fakecloud_core::multi_account::MultiAccountState::new(
1839 "123456789012",
1840 "us-east-1",
1841 "",
1842 ),
1843 )),
1844 lambda: Arc::new(RwLock::new(
1845 fakecloud_core::multi_account::MultiAccountState::new(
1846 "123456789012",
1847 "us-east-1",
1848 "",
1849 ),
1850 )),
1851 secretsmanager: Arc::new(RwLock::new(
1852 fakecloud_core::multi_account::MultiAccountState::new(
1853 "123456789012",
1854 "us-east-1",
1855 "",
1856 ),
1857 )),
1858 kinesis: Arc::new(RwLock::new(
1859 fakecloud_core::multi_account::MultiAccountState::new(
1860 "123456789012",
1861 "us-east-1",
1862 "",
1863 ),
1864 )),
1865 kms: Arc::new(RwLock::new(
1866 fakecloud_core::multi_account::MultiAccountState::new(
1867 "123456789012",
1868 "us-east-1",
1869 "",
1870 ),
1871 )),
1872 ecr: Arc::new(RwLock::new(
1873 fakecloud_core::multi_account::MultiAccountState::new(
1874 "123456789012",
1875 "us-east-1",
1876 "",
1877 ),
1878 )),
1879 cloudwatch: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
1880 elbv2: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
1881 organizations: Arc::new(RwLock::new(None)),
1882 cognito: Arc::new(RwLock::new(
1883 fakecloud_core::multi_account::MultiAccountState::new(
1884 "123456789012",
1885 "us-east-1",
1886 "",
1887 ),
1888 )),
1889 rds: Arc::new(RwLock::new(
1890 fakecloud_core::multi_account::MultiAccountState::new(
1891 "123456789012",
1892 "us-east-1",
1893 "",
1894 ),
1895 )),
1896 ecs: Arc::new(RwLock::new(
1897 fakecloud_core::multi_account::MultiAccountState::new(
1898 "123456789012",
1899 "us-east-1",
1900 "",
1901 ),
1902 )),
1903 acm: Arc::new(RwLock::new(fakecloud_acm::AcmAccounts::new())),
1904 elasticache: Arc::new(RwLock::new(
1905 fakecloud_core::multi_account::MultiAccountState::new(
1906 "123456789012",
1907 "us-east-1",
1908 "",
1909 ),
1910 )),
1911 route53: Arc::new(RwLock::new(fakecloud_route53::Route53Accounts::new())),
1912 cloudfront: Arc::new(RwLock::new(fakecloud_cloudfront::CloudFrontAccounts::new())),
1913 stepfunctions: Arc::new(RwLock::new(
1914 fakecloud_core::multi_account::MultiAccountState::new(
1915 "123456789012",
1916 "us-east-1",
1917 "",
1918 ),
1919 )),
1920 wafv2: Arc::new(RwLock::new(fakecloud_wafv2::Wafv2Accounts::default())),
1921 apigateway: Arc::new(RwLock::new(
1922 fakecloud_core::multi_account::MultiAccountState::new(
1923 "123456789012",
1924 "us-east-1",
1925 "",
1926 ),
1927 )),
1928 apigatewayv2: Arc::new(RwLock::new(
1929 fakecloud_core::multi_account::MultiAccountState::new(
1930 "123456789012",
1931 "us-east-1",
1932 "",
1933 ),
1934 )),
1935 ses: Arc::new(RwLock::new(
1936 fakecloud_core::multi_account::MultiAccountState::new(
1937 "123456789012",
1938 "us-east-1",
1939 "",
1940 ),
1941 )),
1942 application_autoscaling: Arc::new(parking_lot::RwLock::new(
1943 fakecloud_application_autoscaling::ApplicationAutoScalingAccounts::new(),
1944 )),
1945 athena: Arc::new(parking_lot::RwLock::new(
1946 fakecloud_athena::AthenaAccounts::new(),
1947 )),
1948 firehose: Arc::new(parking_lot::RwLock::new(
1949 fakecloud_firehose::FirehoseAccounts::new(),
1950 )),
1951 glue: Arc::new(parking_lot::RwLock::new(fakecloud_glue::GlueAccounts::new())),
1952 delivery: Arc::new(DeliveryBus::new()),
1953 lambda_runtime: None,
1954 };
1955 CloudFormationService::new(cf_state, deps)
1956 }
1957
1958 fn make_request(action: &str, params: HashMap<String, String>) -> AwsRequest {
1959 AwsRequest {
1960 service: "cloudformation".to_string(),
1961 action: action.to_string(),
1962 region: "us-east-1".to_string(),
1963 account_id: "123456789012".to_string(),
1964 request_id: "test-request-id".to_string(),
1965 headers: HeaderMap::new(),
1966 query_params: params,
1967 body: bytes::Bytes::new(),
1968 body_stream: parking_lot::Mutex::new(None),
1969 path_segments: vec![],
1970 raw_path: "/".to_string(),
1971 raw_query: String::new(),
1972 method: http::Method::POST,
1973 is_query_protocol: true,
1974 access_key_id: None,
1975 principal: None,
1976 }
1977 }
1978
1979 #[test]
1980 fn update_stack_sets_failed_status_on_resource_error() {
1981 let svc = make_service();
1982
1983 let mut create_params = HashMap::new();
1985 create_params.insert("StackName".to_string(), "test-stack".to_string());
1986 create_params.insert(
1987 "TemplateBody".to_string(),
1988 r#"{"Resources":{"MyQueue":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"q1"}}}}"#.to_string(),
1989 );
1990 let req = make_request("CreateStack", create_params);
1991 let result = svc.create_stack(&req);
1992 assert!(result.is_ok());
1993
1994 let mut update_params = HashMap::new();
1996 update_params.insert("StackName".to_string(), "test-stack".to_string());
1997 update_params.insert(
1998 "TemplateBody".to_string(),
1999 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(),
2000 );
2001 let req = make_request("UpdateStack", update_params);
2002 let result = svc.update_stack(&req);
2003
2004 assert!(result.is_err());
2006
2007 let accounts = svc.state.read();
2011 let state = accounts.get("123456789012").unwrap();
2012 let stack = state.stacks.get("test-stack").unwrap();
2013 assert_eq!(stack.status, "UPDATE_ROLLBACK_COMPLETE");
2014 }
2015
2016 #[test]
2017 fn create_stack_resolves_ref_to_physical_id() {
2018 let svc = make_service();
2019
2020 let template = r#"{
2022 "Resources": {
2023 "MyTopic": {
2024 "Type": "AWS::SNS::Topic",
2025 "Properties": { "TopicName": "ref-test-topic" }
2026 },
2027 "MySub": {
2028 "Type": "AWS::SNS::Subscription",
2029 "Properties": {
2030 "TopicArn": { "Ref": "MyTopic" },
2031 "Protocol": "sqs",
2032 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:some-queue"
2033 }
2034 }
2035 }
2036 }"#;
2037
2038 let mut params = HashMap::new();
2039 params.insert("StackName".to_string(), "ref-stack".to_string());
2040 params.insert("TemplateBody".to_string(), template.to_string());
2041 let req = make_request("CreateStack", params);
2042 let result = svc.create_stack(&req);
2043 assert!(result.is_ok(), "CreateStack failed: {:?}", result.err());
2044
2045 let accounts = svc.state.read();
2047 let state = accounts.get("123456789012").unwrap();
2048 let stack = state.stacks.get("ref-stack").unwrap();
2049 assert_eq!(stack.resources.len(), 2);
2050 assert_eq!(stack.status, "CREATE_COMPLETE");
2051
2052 let sub = stack
2054 .resources
2055 .iter()
2056 .find(|r| r.logical_id == "MySub")
2057 .unwrap();
2058 assert!(
2059 sub.physical_id.contains("ref-test-topic"),
2060 "Subscription physical ID should reference the topic ARN, got: {}",
2061 sub.physical_id
2062 );
2063 }
2064
2065 #[test]
2068 fn create_stack_missing_name_errors() {
2069 let svc = make_service();
2070 let mut params = HashMap::new();
2071 params.insert("TemplateBody".to_string(), "{}".to_string());
2072 let req = make_request("CreateStack", params);
2073 assert!(svc.create_stack(&req).is_err());
2074 }
2075
2076 #[test]
2077 fn create_stack_missing_template_creates_empty_stack() {
2078 let svc = make_service();
2083 let mut params = HashMap::new();
2084 params.insert("StackName".to_string(), "s".to_string());
2085 let req = make_request("CreateStack", params);
2086 svc.create_stack(&req).expect("empty-body create succeeds");
2087 }
2088
2089 #[test]
2090 fn create_stack_duplicate_errors() {
2091 let svc = make_service();
2092 let mut params = HashMap::new();
2093 params.insert("StackName".to_string(), "dup".to_string());
2094 params.insert(
2095 "TemplateBody".to_string(),
2096 r#"{"Resources":{"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"dq"}}}}"#
2097 .to_string(),
2098 );
2099 let req = make_request("CreateStack", params.clone());
2100 svc.create_stack(&req).unwrap();
2101 let req = make_request("CreateStack", params);
2102 assert!(svc.create_stack(&req).is_err());
2103 }
2104
2105 #[test]
2106 fn create_stack_invalid_template_creates_empty_stack() {
2107 let svc = make_service();
2111 let mut params = HashMap::new();
2112 params.insert("StackName".to_string(), "bad".to_string());
2113 params.insert("TemplateBody".to_string(), "not json".to_string());
2114 let req = make_request("CreateStack", params);
2115 svc.create_stack(&req).expect("bad-body create succeeds");
2116 }
2117
2118 #[test]
2119 fn delete_stack_unknown_is_noop() {
2120 let svc = make_service();
2121 let mut params = HashMap::new();
2122 params.insert("StackName".to_string(), "ghost".to_string());
2123 let req = make_request("DeleteStack", params);
2124 assert!(svc.delete_stack(&req).is_ok());
2125 }
2126
2127 #[test]
2128 fn describe_stacks_nonexistent_errors() {
2129 let svc = make_service();
2134 let mut params = HashMap::new();
2135 params.insert("StackName".to_string(), "ghost".to_string());
2136 let req = make_request("DescribeStacks", params);
2137 match svc.describe_stacks(&req) {
2138 Ok(_) => panic!("ghost stack must return an error, not an empty list"),
2139 Err(e) => {
2140 assert_eq!(e.status(), StatusCode::BAD_REQUEST);
2141 assert_eq!(e.code(), "ValidationError");
2142 assert!(
2143 e.message().contains("does not exist"),
2144 "got: {}",
2145 e.message()
2146 );
2147 }
2148 }
2149 }
2150
2151 #[test]
2152 fn describe_stacks_empty_returns_all() {
2153 let svc = make_service();
2154 let req = make_request("DescribeStacks", HashMap::new());
2155 let resp = svc.describe_stacks(&req).unwrap();
2156 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
2157 assert!(b.contains("DescribeStacksResult"));
2158 }
2159
2160 #[test]
2161 fn list_stacks_empty_returns_ok() {
2162 let svc = make_service();
2163 let req = make_request("ListStacks", HashMap::new());
2164 let resp = svc.list_stacks(&req).unwrap();
2165 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
2166 assert!(b.contains("ListStacksResult"));
2167 }
2168
2169 #[test]
2170 fn list_stack_resources_missing_name_returns_validation_error() {
2171 let svc = make_service();
2177 let req = make_request("ListStackResources", HashMap::new());
2178 let err = match svc.list_stack_resources(&req) {
2179 Err(e) => e,
2180 Ok(_) => panic!("omitted StackName must be rejected"),
2181 };
2182 assert_eq!(err.code(), "ValidationError");
2183 }
2184
2185 #[test]
2186 fn list_stack_resources_unknown_stack_returns_empty() {
2187 let svc = make_service();
2188 let mut params = HashMap::new();
2189 params.insert("StackName".to_string(), "ghost".to_string());
2190 let req = make_request("ListStackResources", params);
2191 svc.list_stack_resources(&req).expect("unknown is empty");
2192 }
2193
2194 #[test]
2195 fn describe_stack_resources_missing_name_returns_empty() {
2196 let svc = make_service();
2197 let req = make_request("DescribeStackResources", HashMap::new());
2198 svc.describe_stack_resources(&req)
2199 .expect("missing name is ok");
2200 }
2201
2202 #[test]
2203 fn get_template_missing_name_returns_empty_body() {
2204 let svc = make_service();
2205 let req = make_request("GetTemplate", HashMap::new());
2206 svc.get_template(&req).expect("missing name is ok");
2207 }
2208
2209 #[test]
2210 fn get_template_unknown_stack_returns_empty_body() {
2211 let svc = make_service();
2212 let mut params = HashMap::new();
2213 params.insert("StackName".to_string(), "ghost".to_string());
2214 let req = make_request("GetTemplate", params);
2215 svc.get_template(&req).expect("unknown is empty");
2216 }
2217
2218 #[test]
2219 fn update_stack_missing_name_errors() {
2220 let svc = make_service();
2221 let mut params = HashMap::new();
2222 params.insert("TemplateBody".to_string(), "{}".to_string());
2223 let req = make_request("UpdateStack", params);
2224 assert!(svc.update_stack(&req).is_err());
2225 }
2226
2227 #[test]
2228 fn update_stack_unknown_stack_returns_synthetic_id() {
2229 let svc = make_service();
2236 let mut params = HashMap::new();
2237 params.insert("StackName".to_string(), "ghost".to_string());
2238 params.insert(
2239 "TemplateBody".to_string(),
2240 r#"{"Resources":{}}"#.to_string(),
2241 );
2242 let req = make_request("UpdateStack", params);
2243 let resp = svc.update_stack(&req).expect("ghost update is synthetic");
2244 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
2245 assert!(b.contains("UpdateStackResult"));
2246 }
2247
2248 #[test]
2249 fn create_stack_resolves_outputs_and_records_export() {
2250 let svc = make_service();
2251 let template = r#"{
2252 "Resources": {
2253 "Q": {"Type":"AWS::SQS::Queue","Properties":{"QueueName":"out-q"}}
2254 },
2255 "Outputs": {
2256 "QueueUrl": {
2257 "Value": {"Ref": "Q"},
2258 "Description": "Url",
2259 "Export": {"Name": "TheQueueUrl"}
2260 }
2261 }
2262 }"#;
2263 let mut params = HashMap::new();
2264 params.insert("StackName".to_string(), "outs".to_string());
2265 params.insert("TemplateBody".to_string(), template.to_string());
2266 let req = make_request("CreateStack", params);
2267 svc.create_stack(&req).expect("create stack");
2268
2269 let accounts = svc.state.read();
2270 let stack = accounts
2271 .get("123456789012")
2272 .unwrap()
2273 .stacks
2274 .get("outs")
2275 .unwrap();
2276 assert_eq!(stack.outputs.len(), 1);
2277 assert_eq!(stack.outputs[0].key, "QueueUrl");
2278 assert_eq!(stack.outputs[0].export_name.as_deref(), Some("TheQueueUrl"));
2279 assert!(!stack.outputs[0].value.is_empty());
2280 }
2281
2282 #[test]
2283 fn create_stack_rejects_duplicate_export_name() {
2284 let svc = make_service();
2285 let mk = |name: &str| {
2286 let template = format!(
2287 r#"{{
2288 "Resources": {{"Q":{{"Type":"AWS::SQS::Queue","Properties":{{"QueueName":"q-{name}"}}}}}},
2289 "Outputs": {{"QueueUrl":{{"Value":{{"Ref":"Q"}},"Export":{{"Name":"DupExport"}}}}}}
2290 }}"#
2291 );
2292 let mut params = HashMap::new();
2293 params.insert("StackName".to_string(), name.to_string());
2294 params.insert("TemplateBody".to_string(), template);
2295 make_request("CreateStack", params)
2296 };
2297 match svc.create_stack(&mk("first")) {
2298 Ok(_) => {}
2299 Err(e) => panic!("first stack: {e:?}"),
2300 }
2301 match svc.create_stack(&mk("second")) {
2302 Ok(_) => panic!("expected duplicate-export error"),
2303 Err(e) => assert!(
2304 format!("{e:?}").contains("already exported"),
2305 "expected duplicate-export error, got {e:?}"
2306 ),
2307 }
2308 }
2309
2310 #[test]
2311 fn import_value_resolves_against_other_stack_export() {
2312 let svc = make_service();
2313
2314 let producer_tpl = r#"{
2315 "Resources": {"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"prod-q"}}},
2316 "Outputs": {"Out":{"Value":{"Ref":"Q"},"Export":{"Name":"SharedQueueUrl"}}}
2317 }"#;
2318 let mut p = HashMap::new();
2319 p.insert("StackName".to_string(), "producer".to_string());
2320 p.insert("TemplateBody".to_string(), producer_tpl.to_string());
2321 svc.create_stack(&make_request("CreateStack", p))
2322 .expect("producer");
2323
2324 let consumer_tpl = r#"{
2325 "Resources": {"Q2":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"cons-q"}}},
2326 "Outputs": {"Imp":{"Value":{"Fn::ImportValue":"SharedQueueUrl"}}}
2327 }"#;
2328 let mut p = HashMap::new();
2329 p.insert("StackName".to_string(), "consumer".to_string());
2330 p.insert("TemplateBody".to_string(), consumer_tpl.to_string());
2331 svc.create_stack(&make_request("CreateStack", p))
2332 .expect("consumer");
2333
2334 let accounts = svc.state.read();
2335 let prod_url = accounts
2336 .get("123456789012")
2337 .unwrap()
2338 .stacks
2339 .get("producer")
2340 .unwrap()
2341 .outputs[0]
2342 .value
2343 .clone();
2344 let cons = accounts
2345 .get("123456789012")
2346 .unwrap()
2347 .stacks
2348 .get("consumer")
2349 .unwrap();
2350 assert_eq!(cons.outputs[0].value, prod_url);
2351 }
2352
2353 #[test]
2354 fn create_stack_records_export_in_state_registry() {
2355 let svc = make_service();
2356 let template = r#"{
2357 "Resources": {"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"reg-q"}}},
2358 "Outputs": {"Url":{"Value":{"Ref":"Q"},"Export":{"Name":"reg-url"}}}
2359 }"#;
2360 let mut params = HashMap::new();
2361 params.insert("StackName".to_string(), "reg".to_string());
2362 params.insert("TemplateBody".to_string(), template.to_string());
2363 svc.create_stack(&make_request("CreateStack", params))
2364 .expect("create");
2365
2366 let accounts = svc.state.read();
2367 let state = accounts.get("123456789012").unwrap();
2368 let export = state
2369 .exports
2370 .get("reg-url")
2371 .expect("export registered in state.exports");
2372 assert_eq!(export.exporting_stack_name, "reg");
2373 assert!(!export.value.is_empty());
2374 assert!(export.exporting_stack_id.contains("reg"));
2375 }
2376
2377 #[test]
2378 fn import_value_with_unknown_export_errors() {
2379 let svc = make_service();
2380 let consumer_tpl = r#"{
2381 "Resources": {"Q":{"Type":"AWS::SQS::Queue","Properties":{
2382 "QueueName": {"Fn::ImportValue":"missing-export"}
2383 }}}
2384 }"#;
2385 let mut p = HashMap::new();
2386 p.insert("StackName".to_string(), "bad-consumer".to_string());
2387 p.insert("TemplateBody".to_string(), consumer_tpl.to_string());
2388 match svc.create_stack(&make_request("CreateStack", p)) {
2389 Ok(_) => panic!("expected ValidationError for unknown export"),
2390 Err(e) => {
2391 let msg = format!("{e:?}");
2392 assert!(msg.contains("No export named missing-export"), "got {msg}");
2393 }
2394 }
2395 }
2396
2397 #[test]
2398 fn delete_stack_blocked_when_export_in_use_and_unblocked_after_consumer_delete() {
2399 let svc = make_service();
2400
2401 let producer_tpl = r#"{
2402 "Resources": {"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"prod"}}},
2403 "Outputs": {"Out":{"Value":{"Ref":"Q"},"Export":{"Name":"my-arn"}}}
2404 }"#;
2405 let mut p = HashMap::new();
2406 p.insert("StackName".to_string(), "producer".to_string());
2407 p.insert("TemplateBody".to_string(), producer_tpl.to_string());
2408 svc.create_stack(&make_request("CreateStack", p))
2409 .expect("producer");
2410
2411 let consumer_tpl = r#"{
2412 "Resources": {"Q2":{"Type":"AWS::SQS::Queue","Properties":{
2413 "QueueName": "cons-q",
2414 "Tags": [{"Key":"k","Value":{"Fn::ImportValue":"my-arn"}}]
2415 }}}
2416 }"#;
2417 let mut p = HashMap::new();
2418 p.insert("StackName".to_string(), "consumer".to_string());
2419 p.insert("TemplateBody".to_string(), consumer_tpl.to_string());
2420 svc.create_stack(&make_request("CreateStack", p))
2421 .expect("consumer");
2422
2423 let mut p = HashMap::new();
2425 p.insert("StackName".to_string(), "producer".to_string());
2426 match svc.delete_stack(&make_request("DeleteStack", p)) {
2427 Ok(_) => panic!("delete must fail while imports exist"),
2428 Err(e) => {
2429 let msg = format!("{e:?}");
2430 assert!(msg.contains("Export my-arn cannot be deleted"), "got {msg}");
2431 }
2432 }
2433
2434 let mut p = HashMap::new();
2436 p.insert("StackName".to_string(), "consumer".to_string());
2437 svc.delete_stack(&make_request("DeleteStack", p))
2438 .expect("consumer delete");
2439
2440 let mut p = HashMap::new();
2442 p.insert("StackName".to_string(), "producer".to_string());
2443 svc.delete_stack(&make_request("DeleteStack", p))
2444 .expect("producer delete after consumer gone");
2445
2446 let accounts = svc.state.read();
2447 let state = accounts.get("123456789012").unwrap();
2448 assert!(state.exports.is_empty(), "exports cleared after delete");
2449 assert!(state.imports.is_empty(), "imports cleared after delete");
2450 }
2451}