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 let _ = stack_name;
916
917 Ok(AwsResponse::xml(
918 StatusCode::OK,
919 xml_responses::describe_stacks_response(&stacks, &req.request_id),
920 ))
921 }
922
923 fn list_stacks(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
924 let accounts = self.state.read();
925 let empty = CloudFormationState::new(&req.account_id, &req.region);
926 let state = accounts.get(&req.account_id).unwrap_or(&empty);
927 let stacks: Vec<Stack> = state.stacks.values().cloned().collect();
928
929 Ok(AwsResponse::xml(
930 StatusCode::OK,
931 xml_responses::list_stacks_response(&stacks, &req.request_id),
932 ))
933 }
934
935 fn list_stack_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
936 let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
942 AwsServiceError::aws_error(
943 StatusCode::BAD_REQUEST,
944 "ValidationError",
945 "StackName is required",
946 )
947 })?;
948
949 let accounts = self.state.read();
950 let empty = CloudFormationState::new(&req.account_id, &req.region);
951 let state = accounts.get(&req.account_id).unwrap_or(&empty);
952 let resources = state
953 .stacks
954 .values()
955 .find(|s| {
956 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
957 })
958 .map(|s| s.resources.clone())
959 .unwrap_or_default();
960
961 Ok(AwsResponse::xml(
962 StatusCode::OK,
963 xml_responses::list_stack_resources_response(&resources, &req.request_id),
964 ))
965 }
966
967 fn describe_stack_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
968 let stack_name = Self::get_param(req, "StackName").unwrap_or_default();
971
972 let accounts = self.state.read();
973 let empty = CloudFormationState::new(&req.account_id, &req.region);
974 let state = accounts.get(&req.account_id).unwrap_or(&empty);
975 let (resources, resolved_name) = state
976 .stacks
977 .values()
978 .find(|s| {
979 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
980 })
981 .map(|s| (s.resources.clone(), s.name.clone()))
982 .unwrap_or_else(|| (Vec::new(), stack_name.clone()));
983
984 Ok(AwsResponse::xml(
985 StatusCode::OK,
986 xml_responses::describe_stack_resources_response(
987 &resources,
988 &resolved_name,
989 &req.request_id,
990 ),
991 ))
992 }
993
994 fn update_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
995 let mut input = UpdateStackInput::from_params(req)?;
996
997 let found_stack_id = {
999 let accounts = self.state.read();
1000 let empty = CloudFormationState::new(&req.account_id, &req.region);
1001 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1002 state
1003 .stacks
1004 .values()
1005 .find(|s| {
1006 (s.name == input.stack_name || s.stack_id == input.stack_name)
1007 && s.status != "DELETE_COMPLETE"
1008 })
1009 .map(|s| s.stack_id.clone())
1010 .unwrap_or_default()
1011 };
1012
1013 input
1017 .parameters
1018 .entry("AWS::Region".to_string())
1019 .or_insert_with(|| req.region.clone());
1020 input
1021 .parameters
1022 .entry("AWS::AccountId".to_string())
1023 .or_insert_with(|| req.account_id.clone());
1024 input
1025 .parameters
1026 .entry("AWS::StackId".to_string())
1027 .or_insert_with(|| found_stack_id.clone());
1028 input
1029 .parameters
1030 .entry("AWS::StackName".to_string())
1031 .or_insert_with(|| input.stack_name.clone());
1032 input
1033 .parameters
1034 .entry("AWS::Partition".to_string())
1035 .or_insert_with(|| template::partition_for_region(&req.region).to_string());
1036 input
1037 .parameters
1038 .entry("AWS::URLSuffix".to_string())
1039 .or_insert_with(|| template::url_suffix_for_region(&req.region).to_string());
1040 if !input.notification_arns.is_empty() {
1045 input.parameters.insert(
1046 "AWS::NotificationARNs".to_string(),
1047 serde_json::to_string(&input.notification_arns)
1048 .unwrap_or_else(|_| "[]".to_string()),
1049 );
1050 } else {
1051 let existing: Vec<String> = {
1054 let accounts = self.state.read();
1055 accounts
1056 .get(&req.account_id)
1057 .and_then(|s| {
1058 s.stacks
1059 .values()
1060 .find(|st| st.stack_id == found_stack_id)
1061 .map(|st| st.notification_arns.clone())
1062 })
1063 .unwrap_or_default()
1064 };
1065 input.parameters.insert(
1066 "AWS::NotificationARNs".to_string(),
1067 serde_json::to_string(&existing).unwrap_or_else(|_| "[]".to_string()),
1068 );
1069 }
1070
1071 let parsed = template::parse_template(&input.template_body, &input.parameters)
1076 .unwrap_or_else(|_| template::ParsedTemplate {
1077 description: None,
1078 resources: Vec::new(),
1079 outputs: Vec::new(),
1080 });
1081
1082 let imported_names = Self::validate_import_values(
1083 &self.state,
1084 &req.account_id,
1085 &input.stack_name,
1086 &input.template_body,
1087 &input.parameters,
1088 )?;
1089
1090 let provisioner = self.provisioner(&found_stack_id, &req.account_id, &req.region);
1091
1092 let mut accounts = self.state.write();
1093 let state = accounts.get_or_create(&req.account_id);
1094 let stack_exists = state.stacks.values().any(|s| {
1103 (s.name == input.stack_name || s.stack_id == input.stack_name)
1104 && s.status != "DELETE_COMPLETE"
1105 });
1106 if !stack_exists {
1107 let stack_id = if found_stack_id.is_empty() {
1108 format!(
1109 "arn:aws:cloudformation:{}:{}:stack/{}/{}",
1110 req.region,
1111 req.account_id,
1112 input.stack_name,
1113 uuid::Uuid::new_v4()
1114 )
1115 } else {
1116 found_stack_id.clone()
1117 };
1118 return Ok(AwsResponse::xml(
1119 StatusCode::OK,
1120 xml_responses::update_stack_response(&stack_id, &req.request_id),
1121 ));
1122 }
1123 let (update_result, stack_id, stack_name_owned, resources_snapshot, notification_arns) = {
1124 let stack = state
1125 .stacks
1126 .values_mut()
1127 .find(|s| {
1128 (s.name == input.stack_name || s.stack_id == input.stack_name)
1129 && s.status != "DELETE_COMPLETE"
1130 })
1131 .expect("stack existence checked above");
1132
1133 stack.status = "UPDATE_IN_PROGRESS".to_string();
1134 let update_result = apply_resource_updates(
1135 stack,
1136 &parsed.resources,
1137 &input.template_body,
1138 &input.parameters,
1139 &provisioner,
1140 );
1141
1142 let stack_id = stack.stack_id.clone();
1143 let stack_name_owned = stack.name.clone();
1144 stack.template = input.template_body.clone();
1145 stack.status = if update_result.is_err() {
1146 "UPDATE_ROLLBACK_COMPLETE".to_string()
1147 } else {
1148 "UPDATE_COMPLETE".to_string()
1149 };
1150 stack.parameters = input.parameters.clone();
1151 if !input.tags.is_empty() {
1152 stack.tags = input.tags;
1153 }
1154 stack.updated_at = Some(Utc::now());
1155 stack.description = parsed.description;
1156 if !input.notification_arns.is_empty() {
1157 stack.notification_arns = input.notification_arns.clone();
1158 }
1159 if update_result.is_ok() {
1160 stack.outputs.clear();
1161 }
1162 (
1163 update_result,
1164 stack_id,
1165 stack_name_owned,
1166 stack.resources.clone(),
1167 stack.notification_arns.clone(),
1168 )
1169 };
1170
1171 record_stack_status_event(
1173 state,
1174 &stack_id,
1175 &stack_name_owned,
1176 "AWS::CloudFormation::Stack",
1177 "UPDATE_IN_PROGRESS",
1178 );
1179 let update_result = match update_result {
1180 Ok(changes) => {
1181 record_stack_events(state, &stack_id, &stack_name_owned, &changes);
1182 record_stack_status_event(
1183 state,
1184 &stack_id,
1185 &stack_name_owned,
1186 "AWS::CloudFormation::Stack",
1187 "UPDATE_COMPLETE",
1188 );
1189 Ok(())
1190 }
1191 Err(e) => {
1192 record_stack_status_event(
1193 state,
1194 &stack_id,
1195 &stack_name_owned,
1196 "AWS::CloudFormation::Stack",
1197 "UPDATE_ROLLBACK_COMPLETE",
1198 );
1199 Err(e)
1200 }
1201 };
1202 let stack_name_for_notif = stack_name_owned.clone();
1203
1204 if let Err(error_msg) = update_result {
1205 drop(accounts);
1206 Self::send_stack_notification(
1207 &self.deps.delivery,
1208 ¬ification_arns,
1209 &stack_name_for_notif,
1210 &stack_id,
1211 "UPDATE_FAILED",
1212 );
1213 return Err(AwsServiceError::aws_error(
1214 StatusCode::BAD_REQUEST,
1215 "InsufficientCapabilitiesException",
1216 error_msg,
1217 ));
1218 }
1219
1220 drop(accounts);
1221
1222 let outputs = Self::resolve_template_outputs(
1223 &input.template_body,
1224 &input.parameters,
1225 &resources_snapshot,
1226 &self.state,
1227 );
1228 Self::ensure_export_uniqueness(&self.state, &req.account_id, &input.stack_name, &outputs)?;
1229 {
1230 let mut accounts = self.state.write();
1231 let state = accounts.get_or_create(&req.account_id);
1232 if let Some(stack) = state
1233 .stacks
1234 .values_mut()
1235 .find(|s| s.stack_id == stack_id && s.status != "DELETE_COMPLETE")
1236 {
1237 stack.outputs = outputs.clone();
1238 }
1239 Self::sync_exports_imports(
1240 state,
1241 &stack_id,
1242 &input.stack_name,
1243 &outputs,
1244 &imported_names,
1245 );
1246 }
1247
1248 Self::send_stack_notification(
1249 &self.deps.delivery,
1250 ¬ification_arns,
1251 &stack_name_for_notif,
1252 &stack_id,
1253 "UPDATE_COMPLETE",
1254 );
1255
1256 Ok(AwsResponse::xml(
1257 StatusCode::OK,
1258 xml_responses::update_stack_response(&stack_id, &req.request_id),
1259 ))
1260 }
1261
1262 fn get_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1263 let stack_name = Self::get_param(req, "StackName").unwrap_or_default();
1265
1266 let accounts = self.state.read();
1267 let empty = CloudFormationState::new(&req.account_id, &req.region);
1268 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1269 let body = state
1274 .stacks
1275 .values()
1276 .find(|s| {
1277 (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
1278 })
1279 .map(|s| s.template.clone())
1280 .unwrap_or_default();
1281
1282 Ok(AwsResponse::xml(
1283 StatusCode::OK,
1284 xml_responses::get_template_response(&body, &req.request_id),
1285 ))
1286 }
1287}
1288
1289#[async_trait]
1290impl AwsService for CloudFormationService {
1291 fn service_name(&self) -> &str {
1292 "cloudformation"
1293 }
1294
1295 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1296 let action = req.action.as_str();
1297
1298 crate::input_constraints::validate_input(action, &Self::get_all_params(&req))?;
1305
1306 let mutates = matches!(
1310 action,
1311 "CreateStack"
1312 | "DeleteStack"
1313 | "UpdateStack"
1314 | "CreateChangeSet"
1315 | "DeleteChangeSet"
1316 | "ExecuteChangeSet"
1317 | "CreateStackSet"
1318 | "DeleteStackSet"
1319 | "CreateStackRefactor"
1320 | "CreateGeneratedTemplate"
1321 | "DeleteGeneratedTemplate"
1322 | "SetStackPolicy"
1323 | "UpdateTerminationProtection"
1324 | "ActivateOrganizationsAccess"
1325 | "DeactivateOrganizationsAccess"
1326 );
1327 let result = match action {
1328 "CreateStack" => self.create_stack(&req),
1329 "DeleteStack" => self.delete_stack(&req),
1330 "DescribeStacks" => self.describe_stacks(&req),
1331 "ListStacks" => self.list_stacks(&req),
1332 "ListStackResources" => self.list_stack_resources(&req),
1333 "DescribeStackResources" => self.describe_stack_resources(&req),
1334 "UpdateStack" => self.update_stack(&req),
1335 "GetTemplate" => self.get_template(&req),
1336 _ => self.handle_extra_action(&req),
1337 };
1338 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
1339 self.save_snapshot().await;
1340 }
1341 result
1342 }
1343
1344 fn supported_actions(&self) -> &[&str] {
1345 &[
1346 "ActivateOrganizationsAccess",
1347 "ActivateType",
1348 "BatchDescribeTypeConfigurations",
1349 "CancelUpdateStack",
1350 "ContinueUpdateRollback",
1351 "CreateChangeSet",
1352 "CreateGeneratedTemplate",
1353 "CreateStack",
1354 "CreateStackInstances",
1355 "CreateStackRefactor",
1356 "CreateStackSet",
1357 "DeactivateOrganizationsAccess",
1358 "DeactivateType",
1359 "DeleteChangeSet",
1360 "DeleteGeneratedTemplate",
1361 "DeleteStack",
1362 "DeleteStackInstances",
1363 "DeleteStackSet",
1364 "DeregisterType",
1365 "DescribeAccountLimits",
1366 "DescribeChangeSet",
1367 "DescribeChangeSetHooks",
1368 "DescribeEvents",
1369 "DescribeGeneratedTemplate",
1370 "DescribeOrganizationsAccess",
1371 "DescribePublisher",
1372 "DescribeResourceScan",
1373 "DescribeStackDriftDetectionStatus",
1374 "DescribeStackEvents",
1375 "DescribeStackInstance",
1376 "DescribeStackRefactor",
1377 "DescribeStackResource",
1378 "DescribeStackResourceDrifts",
1379 "DescribeStackResources",
1380 "DescribeStackSet",
1381 "DescribeStackSetOperation",
1382 "DescribeStacks",
1383 "DescribeType",
1384 "DescribeTypeRegistration",
1385 "DetectStackDrift",
1386 "DetectStackResourceDrift",
1387 "DetectStackSetDrift",
1388 "EstimateTemplateCost",
1389 "ExecuteChangeSet",
1390 "ExecuteStackRefactor",
1391 "GetGeneratedTemplate",
1392 "GetHookResult",
1393 "GetStackPolicy",
1394 "GetTemplate",
1395 "GetTemplateSummary",
1396 "ImportStacksToStackSet",
1397 "ListChangeSets",
1398 "ListExports",
1399 "ListGeneratedTemplates",
1400 "ListHookResults",
1401 "ListImports",
1402 "ListResourceScanRelatedResources",
1403 "ListResourceScanResources",
1404 "ListResourceScans",
1405 "ListStackInstanceResourceDrifts",
1406 "ListStackInstances",
1407 "ListStackRefactorActions",
1408 "ListStackRefactors",
1409 "ListStackResources",
1410 "ListStackSetAutoDeploymentTargets",
1411 "ListStackSetOperationResults",
1412 "ListStackSetOperations",
1413 "ListStackSets",
1414 "ListStacks",
1415 "ListTypeRegistrations",
1416 "ListTypeVersions",
1417 "ListTypes",
1418 "PublishType",
1419 "RecordHandlerProgress",
1420 "RegisterPublisher",
1421 "RegisterType",
1422 "RollbackStack",
1423 "SetStackPolicy",
1424 "SetTypeConfiguration",
1425 "SetTypeDefaultVersion",
1426 "SignalResource",
1427 "StartResourceScan",
1428 "StopStackSetOperation",
1429 "TestType",
1430 "UpdateGeneratedTemplate",
1431 "UpdateStack",
1432 "UpdateStackInstances",
1433 "UpdateStackSet",
1434 "UpdateTerminationProtection",
1435 "ValidateTemplate",
1436 ]
1437 }
1438}
1439
1440struct UpdateStackInput {
1442 stack_name: String,
1443 template_body: String,
1444 parameters: BTreeMap<String, String>,
1445 tags: BTreeMap<String, String>,
1446 notification_arns: Vec<String>,
1447}
1448
1449impl UpdateStackInput {
1450 fn from_params(req: &AwsRequest) -> Result<Self, AwsServiceError> {
1451 let params = CloudFormationService::get_all_params(req);
1452
1453 let stack_name = params
1454 .get("StackName")
1455 .ok_or_else(|| {
1456 AwsServiceError::aws_error(
1457 StatusCode::BAD_REQUEST,
1458 "ValidationError",
1459 "StackName is required",
1460 )
1461 })?
1462 .to_string();
1463
1464 let template_body = params.get("TemplateBody").cloned().unwrap_or_default();
1469
1470 Ok(Self {
1471 stack_name,
1472 template_body,
1473 parameters: CloudFormationService::extract_parameters(¶ms),
1474 tags: CloudFormationService::extract_tags(¶ms),
1475 notification_arns: CloudFormationService::extract_notification_arns(¶ms),
1476 })
1477 }
1478}
1479
1480#[derive(Debug, Clone)]
1484pub(crate) struct ResourceChange {
1485 pub action: ResourceChangeAction,
1486 pub logical_id: String,
1487 pub physical_id: String,
1488 pub resource_type: String,
1489}
1490
1491#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1492pub(crate) enum ResourceChangeAction {
1493 Create,
1494 Update,
1495 Delete,
1496}
1497
1498impl ResourceChangeAction {
1499 pub fn status_in_progress(self) -> &'static str {
1500 match self {
1501 Self::Create => "CREATE_IN_PROGRESS",
1502 Self::Update => "UPDATE_IN_PROGRESS",
1503 Self::Delete => "DELETE_IN_PROGRESS",
1504 }
1505 }
1506 pub fn status_complete(self) -> &'static str {
1507 match self {
1508 Self::Create => "CREATE_COMPLETE",
1509 Self::Update => "UPDATE_COMPLETE",
1510 Self::Delete => "DELETE_COMPLETE",
1511 }
1512 }
1513}
1514
1515pub(crate) fn apply_resource_updates(
1520 stack: &mut crate::state::Stack,
1521 new_resource_defs: &[template::ResourceDefinition],
1522 template_body: &str,
1523 parameters: &BTreeMap<String, String>,
1524 provisioner: &crate::resource_provisioner::ResourceProvisioner,
1525) -> Result<Vec<ResourceChange>, String> {
1526 let mut changes: Vec<ResourceChange> = Vec::new();
1527 let old_logical_ids: std::collections::HashSet<String> = stack
1528 .resources
1529 .iter()
1530 .map(|r| r.logical_id.clone())
1531 .collect();
1532 let new_logical_ids: std::collections::HashSet<String> = new_resource_defs
1533 .iter()
1534 .map(|r| r.logical_id.clone())
1535 .collect();
1536
1537 let to_remove: Vec<_> = stack
1539 .resources
1540 .iter()
1541 .filter(|r| !new_logical_ids.contains(&r.logical_id))
1542 .cloned()
1543 .collect();
1544 for resource in &to_remove {
1545 let _ = provisioner.delete_resource(resource);
1546 changes.push(ResourceChange {
1547 action: ResourceChangeAction::Delete,
1548 logical_id: resource.logical_id.clone(),
1549 physical_id: resource.physical_id.clone(),
1550 resource_type: resource.resource_type.clone(),
1551 });
1552 }
1553 stack
1554 .resources
1555 .retain(|r| new_logical_ids.contains(&r.logical_id));
1556
1557 let mut physical_ids: BTreeMap<String, String> = stack
1559 .resources
1560 .iter()
1561 .map(|r| (r.logical_id.clone(), r.physical_id.clone()))
1562 .collect();
1563 let mut attributes: BTreeMap<String, BTreeMap<String, String>> = stack
1564 .resources
1565 .iter()
1566 .map(|r| (r.logical_id.clone(), r.attributes.clone()))
1567 .collect();
1568
1569 for resource_def in new_resource_defs {
1571 let resolved_def = template::resolve_resource_properties_with_attrs(
1572 resource_def,
1573 template_body,
1574 parameters,
1575 &physical_ids,
1576 &attributes,
1577 )
1578 .map_err(|e| {
1579 format!(
1580 "Failed to resolve resource {}: {e}",
1581 resource_def.logical_id
1582 )
1583 })?;
1584
1585 if !old_logical_ids.contains(&resource_def.logical_id) {
1586 match provisioner.create_resource(&resolved_def) {
1587 Ok(stack_resource) => {
1588 changes.push(ResourceChange {
1589 action: ResourceChangeAction::Create,
1590 logical_id: stack_resource.logical_id.clone(),
1591 physical_id: stack_resource.physical_id.clone(),
1592 resource_type: stack_resource.resource_type.clone(),
1593 });
1594 physical_ids.insert(
1595 stack_resource.logical_id.clone(),
1596 stack_resource.physical_id.clone(),
1597 );
1598 attributes.insert(
1599 stack_resource.logical_id.clone(),
1600 stack_resource.attributes.clone(),
1601 );
1602 stack.resources.push(stack_resource);
1603 }
1604 Err(e) => {
1605 tracing::warn!(
1606 "Failed to create resource {} during update: {e}",
1607 resource_def.logical_id
1608 );
1609 return Err(format!(
1610 "Failed to create resource {}: {e}",
1611 resource_def.logical_id
1612 ));
1613 }
1614 }
1615 } else {
1616 let existing = stack
1622 .resources
1623 .iter()
1624 .find(|r| r.logical_id == resource_def.logical_id)
1625 .cloned();
1626 if let Some(existing) = existing {
1627 match provisioner.update_resource(&existing, &resolved_def) {
1628 Ok(Some(updated)) => {
1629 changes.push(ResourceChange {
1630 action: ResourceChangeAction::Update,
1631 logical_id: updated.logical_id.clone(),
1632 physical_id: updated.physical_id.clone(),
1633 resource_type: updated.resource_type.clone(),
1634 });
1635 physical_ids
1636 .insert(updated.logical_id.clone(), updated.physical_id.clone());
1637 attributes.insert(updated.logical_id.clone(), updated.attributes.clone());
1638 if let Some(slot) = stack
1639 .resources
1640 .iter_mut()
1641 .find(|r| r.logical_id == updated.logical_id)
1642 {
1643 *slot = updated;
1644 }
1645 }
1646 Ok(None) => {
1647 }
1650 Err(e) => {
1651 tracing::warn!(
1652 "Failed to update resource {} during update: {e}",
1653 resource_def.logical_id
1654 );
1655 return Err(format!(
1656 "Failed to update resource {}: {e}",
1657 resource_def.logical_id
1658 ));
1659 }
1660 }
1661 }
1662 }
1663 }
1664
1665 Ok(changes)
1666}
1667
1668pub(crate) fn record_event(
1672 state: &mut crate::state::CloudFormationState,
1673 stack_id: &str,
1674 stack_name: &str,
1675 logical_id: &str,
1676 physical_id: &str,
1677 resource_type: &str,
1678 status: &str,
1679) {
1680 use serde_json::json;
1681 let event_id = format!(
1682 "{}-{:x}",
1683 logical_id,
1684 std::time::SystemTime::now()
1685 .duration_since(std::time::UNIX_EPOCH)
1686 .map(|d| d.as_nanos())
1687 .unwrap_or(0)
1688 );
1689 let entry = json!({
1690 "EventId": event_id,
1691 "StackId": stack_id,
1692 "StackName": stack_name,
1693 "LogicalResourceId": logical_id,
1694 "PhysicalResourceId": physical_id,
1695 "ResourceType": resource_type,
1696 "ResourceStatus": status,
1697 "Timestamp": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1698 });
1699 state
1700 .events
1701 .entry(stack_id.to_string())
1702 .or_default()
1703 .push(entry);
1704}
1705
1706pub(crate) fn record_stack_events(
1710 state: &mut crate::state::CloudFormationState,
1711 stack_id: &str,
1712 stack_name: &str,
1713 changes: &[ResourceChange],
1714) {
1715 for ch in changes {
1716 record_event(
1717 state,
1718 stack_id,
1719 stack_name,
1720 &ch.logical_id,
1721 &ch.physical_id,
1722 &ch.resource_type,
1723 ch.action.status_in_progress(),
1724 );
1725 record_event(
1726 state,
1727 stack_id,
1728 stack_name,
1729 &ch.logical_id,
1730 &ch.physical_id,
1731 &ch.resource_type,
1732 ch.action.status_complete(),
1733 );
1734 }
1735}
1736
1737pub(crate) fn record_stack_status_event(
1741 state: &mut crate::state::CloudFormationState,
1742 stack_id: &str,
1743 stack_name: &str,
1744 resource_type: &str,
1745 status: &str,
1746) {
1747 record_event(
1748 state,
1749 stack_id,
1750 stack_name,
1751 stack_name,
1752 stack_id,
1753 resource_type,
1754 status,
1755 );
1756}
1757
1758#[cfg(test)]
1759mod tests {
1760 use super::*;
1761 use http::HeaderMap;
1762 use parking_lot::RwLock;
1763 use std::collections::HashMap;
1764 use std::sync::Arc;
1765
1766 fn make_service() -> CloudFormationService {
1767 let cf_state = Arc::new(RwLock::new(
1768 fakecloud_core::multi_account::MultiAccountState::new(
1769 "123456789012",
1770 "us-east-1",
1771 "http://localhost:4566",
1772 ),
1773 ));
1774 let deps = CloudFormationDeps {
1775 sqs: Arc::new(RwLock::new(
1776 fakecloud_core::multi_account::MultiAccountState::new(
1777 "123456789012",
1778 "us-east-1",
1779 "http://localhost:4566",
1780 ),
1781 )),
1782 sns: Arc::new(RwLock::new(
1783 fakecloud_core::multi_account::MultiAccountState::new(
1784 "123456789012",
1785 "us-east-1",
1786 "http://localhost:4566",
1787 ),
1788 )),
1789 ssm: Arc::new(RwLock::new(
1790 fakecloud_core::multi_account::MultiAccountState::new(
1791 "123456789012",
1792 "us-east-1",
1793 "http://localhost:4566",
1794 ),
1795 )),
1796 iam: Arc::new(RwLock::new(
1797 fakecloud_core::multi_account::MultiAccountState::new(
1798 "123456789012",
1799 "us-east-1",
1800 "",
1801 ),
1802 )),
1803 s3: Arc::new(RwLock::new(
1804 fakecloud_core::multi_account::MultiAccountState::new(
1805 "123456789012",
1806 "us-east-1",
1807 "",
1808 ),
1809 )),
1810 eventbridge: Arc::new(RwLock::new(
1811 fakecloud_core::multi_account::MultiAccountState::new(
1812 "123456789012",
1813 "us-east-1",
1814 "",
1815 ),
1816 )),
1817 dynamodb: Arc::new(RwLock::new(
1818 fakecloud_core::multi_account::MultiAccountState::new(
1819 "123456789012",
1820 "us-east-1",
1821 "",
1822 ),
1823 )),
1824 logs: Arc::new(RwLock::new(
1825 fakecloud_core::multi_account::MultiAccountState::new(
1826 "123456789012",
1827 "us-east-1",
1828 "",
1829 ),
1830 )),
1831 lambda: Arc::new(RwLock::new(
1832 fakecloud_core::multi_account::MultiAccountState::new(
1833 "123456789012",
1834 "us-east-1",
1835 "",
1836 ),
1837 )),
1838 secretsmanager: Arc::new(RwLock::new(
1839 fakecloud_core::multi_account::MultiAccountState::new(
1840 "123456789012",
1841 "us-east-1",
1842 "",
1843 ),
1844 )),
1845 kinesis: Arc::new(RwLock::new(
1846 fakecloud_core::multi_account::MultiAccountState::new(
1847 "123456789012",
1848 "us-east-1",
1849 "",
1850 ),
1851 )),
1852 kms: Arc::new(RwLock::new(
1853 fakecloud_core::multi_account::MultiAccountState::new(
1854 "123456789012",
1855 "us-east-1",
1856 "",
1857 ),
1858 )),
1859 ecr: Arc::new(RwLock::new(
1860 fakecloud_core::multi_account::MultiAccountState::new(
1861 "123456789012",
1862 "us-east-1",
1863 "",
1864 ),
1865 )),
1866 cloudwatch: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
1867 elbv2: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
1868 organizations: Arc::new(RwLock::new(None)),
1869 cognito: Arc::new(RwLock::new(
1870 fakecloud_core::multi_account::MultiAccountState::new(
1871 "123456789012",
1872 "us-east-1",
1873 "",
1874 ),
1875 )),
1876 rds: Arc::new(RwLock::new(
1877 fakecloud_core::multi_account::MultiAccountState::new(
1878 "123456789012",
1879 "us-east-1",
1880 "",
1881 ),
1882 )),
1883 ecs: Arc::new(RwLock::new(
1884 fakecloud_core::multi_account::MultiAccountState::new(
1885 "123456789012",
1886 "us-east-1",
1887 "",
1888 ),
1889 )),
1890 acm: Arc::new(RwLock::new(fakecloud_acm::AcmAccounts::new())),
1891 elasticache: Arc::new(RwLock::new(
1892 fakecloud_core::multi_account::MultiAccountState::new(
1893 "123456789012",
1894 "us-east-1",
1895 "",
1896 ),
1897 )),
1898 route53: Arc::new(RwLock::new(fakecloud_route53::Route53Accounts::new())),
1899 cloudfront: Arc::new(RwLock::new(fakecloud_cloudfront::CloudFrontAccounts::new())),
1900 stepfunctions: Arc::new(RwLock::new(
1901 fakecloud_core::multi_account::MultiAccountState::new(
1902 "123456789012",
1903 "us-east-1",
1904 "",
1905 ),
1906 )),
1907 wafv2: Arc::new(RwLock::new(fakecloud_wafv2::Wafv2Accounts::default())),
1908 apigateway: Arc::new(RwLock::new(
1909 fakecloud_core::multi_account::MultiAccountState::new(
1910 "123456789012",
1911 "us-east-1",
1912 "",
1913 ),
1914 )),
1915 apigatewayv2: Arc::new(RwLock::new(
1916 fakecloud_core::multi_account::MultiAccountState::new(
1917 "123456789012",
1918 "us-east-1",
1919 "",
1920 ),
1921 )),
1922 ses: Arc::new(RwLock::new(
1923 fakecloud_core::multi_account::MultiAccountState::new(
1924 "123456789012",
1925 "us-east-1",
1926 "",
1927 ),
1928 )),
1929 application_autoscaling: Arc::new(parking_lot::RwLock::new(
1930 fakecloud_application_autoscaling::ApplicationAutoScalingAccounts::new(),
1931 )),
1932 athena: Arc::new(parking_lot::RwLock::new(
1933 fakecloud_athena::AthenaAccounts::new(),
1934 )),
1935 firehose: Arc::new(parking_lot::RwLock::new(
1936 fakecloud_firehose::FirehoseAccounts::new(),
1937 )),
1938 glue: Arc::new(parking_lot::RwLock::new(fakecloud_glue::GlueAccounts::new())),
1939 delivery: Arc::new(DeliveryBus::new()),
1940 lambda_runtime: None,
1941 };
1942 CloudFormationService::new(cf_state, deps)
1943 }
1944
1945 fn make_request(action: &str, params: HashMap<String, String>) -> AwsRequest {
1946 AwsRequest {
1947 service: "cloudformation".to_string(),
1948 action: action.to_string(),
1949 region: "us-east-1".to_string(),
1950 account_id: "123456789012".to_string(),
1951 request_id: "test-request-id".to_string(),
1952 headers: HeaderMap::new(),
1953 query_params: params,
1954 body: bytes::Bytes::new(),
1955 body_stream: parking_lot::Mutex::new(None),
1956 path_segments: vec![],
1957 raw_path: "/".to_string(),
1958 raw_query: String::new(),
1959 method: http::Method::POST,
1960 is_query_protocol: true,
1961 access_key_id: None,
1962 principal: None,
1963 }
1964 }
1965
1966 #[test]
1967 fn update_stack_sets_failed_status_on_resource_error() {
1968 let svc = make_service();
1969
1970 let mut create_params = HashMap::new();
1972 create_params.insert("StackName".to_string(), "test-stack".to_string());
1973 create_params.insert(
1974 "TemplateBody".to_string(),
1975 r#"{"Resources":{"MyQueue":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"q1"}}}}"#.to_string(),
1976 );
1977 let req = make_request("CreateStack", create_params);
1978 let result = svc.create_stack(&req);
1979 assert!(result.is_ok());
1980
1981 let mut update_params = HashMap::new();
1983 update_params.insert("StackName".to_string(), "test-stack".to_string());
1984 update_params.insert(
1985 "TemplateBody".to_string(),
1986 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(),
1987 );
1988 let req = make_request("UpdateStack", update_params);
1989 let result = svc.update_stack(&req);
1990
1991 assert!(result.is_err());
1993
1994 let accounts = svc.state.read();
1998 let state = accounts.get("123456789012").unwrap();
1999 let stack = state.stacks.get("test-stack").unwrap();
2000 assert_eq!(stack.status, "UPDATE_ROLLBACK_COMPLETE");
2001 }
2002
2003 #[test]
2004 fn create_stack_resolves_ref_to_physical_id() {
2005 let svc = make_service();
2006
2007 let template = r#"{
2009 "Resources": {
2010 "MyTopic": {
2011 "Type": "AWS::SNS::Topic",
2012 "Properties": { "TopicName": "ref-test-topic" }
2013 },
2014 "MySub": {
2015 "Type": "AWS::SNS::Subscription",
2016 "Properties": {
2017 "TopicArn": { "Ref": "MyTopic" },
2018 "Protocol": "sqs",
2019 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:some-queue"
2020 }
2021 }
2022 }
2023 }"#;
2024
2025 let mut params = HashMap::new();
2026 params.insert("StackName".to_string(), "ref-stack".to_string());
2027 params.insert("TemplateBody".to_string(), template.to_string());
2028 let req = make_request("CreateStack", params);
2029 let result = svc.create_stack(&req);
2030 assert!(result.is_ok(), "CreateStack failed: {:?}", result.err());
2031
2032 let accounts = svc.state.read();
2034 let state = accounts.get("123456789012").unwrap();
2035 let stack = state.stacks.get("ref-stack").unwrap();
2036 assert_eq!(stack.resources.len(), 2);
2037 assert_eq!(stack.status, "CREATE_COMPLETE");
2038
2039 let sub = stack
2041 .resources
2042 .iter()
2043 .find(|r| r.logical_id == "MySub")
2044 .unwrap();
2045 assert!(
2046 sub.physical_id.contains("ref-test-topic"),
2047 "Subscription physical ID should reference the topic ARN, got: {}",
2048 sub.physical_id
2049 );
2050 }
2051
2052 #[test]
2055 fn create_stack_missing_name_errors() {
2056 let svc = make_service();
2057 let mut params = HashMap::new();
2058 params.insert("TemplateBody".to_string(), "{}".to_string());
2059 let req = make_request("CreateStack", params);
2060 assert!(svc.create_stack(&req).is_err());
2061 }
2062
2063 #[test]
2064 fn create_stack_missing_template_creates_empty_stack() {
2065 let svc = make_service();
2070 let mut params = HashMap::new();
2071 params.insert("StackName".to_string(), "s".to_string());
2072 let req = make_request("CreateStack", params);
2073 svc.create_stack(&req).expect("empty-body create succeeds");
2074 }
2075
2076 #[test]
2077 fn create_stack_duplicate_errors() {
2078 let svc = make_service();
2079 let mut params = HashMap::new();
2080 params.insert("StackName".to_string(), "dup".to_string());
2081 params.insert(
2082 "TemplateBody".to_string(),
2083 r#"{"Resources":{"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"dq"}}}}"#
2084 .to_string(),
2085 );
2086 let req = make_request("CreateStack", params.clone());
2087 svc.create_stack(&req).unwrap();
2088 let req = make_request("CreateStack", params);
2089 assert!(svc.create_stack(&req).is_err());
2090 }
2091
2092 #[test]
2093 fn create_stack_invalid_template_creates_empty_stack() {
2094 let svc = make_service();
2098 let mut params = HashMap::new();
2099 params.insert("StackName".to_string(), "bad".to_string());
2100 params.insert("TemplateBody".to_string(), "not json".to_string());
2101 let req = make_request("CreateStack", params);
2102 svc.create_stack(&req).expect("bad-body create succeeds");
2103 }
2104
2105 #[test]
2106 fn delete_stack_unknown_is_noop() {
2107 let svc = make_service();
2108 let mut params = HashMap::new();
2109 params.insert("StackName".to_string(), "ghost".to_string());
2110 let req = make_request("DeleteStack", params);
2111 assert!(svc.delete_stack(&req).is_ok());
2112 }
2113
2114 #[test]
2115 fn describe_stacks_nonexistent_returns_empty() {
2116 let svc = make_service();
2120 let mut params = HashMap::new();
2121 params.insert("StackName".to_string(), "ghost".to_string());
2122 let req = make_request("DescribeStacks", params);
2123 let resp = svc.describe_stacks(&req).expect("ghost is empty");
2124 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
2125 assert!(b.contains("DescribeStacksResult"));
2126 }
2127
2128 #[test]
2129 fn describe_stacks_empty_returns_all() {
2130 let svc = make_service();
2131 let req = make_request("DescribeStacks", HashMap::new());
2132 let resp = svc.describe_stacks(&req).unwrap();
2133 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
2134 assert!(b.contains("DescribeStacksResult"));
2135 }
2136
2137 #[test]
2138 fn list_stacks_empty_returns_ok() {
2139 let svc = make_service();
2140 let req = make_request("ListStacks", HashMap::new());
2141 let resp = svc.list_stacks(&req).unwrap();
2142 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
2143 assert!(b.contains("ListStacksResult"));
2144 }
2145
2146 #[test]
2147 fn list_stack_resources_missing_name_returns_validation_error() {
2148 let svc = make_service();
2154 let req = make_request("ListStackResources", HashMap::new());
2155 let err = match svc.list_stack_resources(&req) {
2156 Err(e) => e,
2157 Ok(_) => panic!("omitted StackName must be rejected"),
2158 };
2159 assert_eq!(err.code(), "ValidationError");
2160 }
2161
2162 #[test]
2163 fn list_stack_resources_unknown_stack_returns_empty() {
2164 let svc = make_service();
2165 let mut params = HashMap::new();
2166 params.insert("StackName".to_string(), "ghost".to_string());
2167 let req = make_request("ListStackResources", params);
2168 svc.list_stack_resources(&req).expect("unknown is empty");
2169 }
2170
2171 #[test]
2172 fn describe_stack_resources_missing_name_returns_empty() {
2173 let svc = make_service();
2174 let req = make_request("DescribeStackResources", HashMap::new());
2175 svc.describe_stack_resources(&req)
2176 .expect("missing name is ok");
2177 }
2178
2179 #[test]
2180 fn get_template_missing_name_returns_empty_body() {
2181 let svc = make_service();
2182 let req = make_request("GetTemplate", HashMap::new());
2183 svc.get_template(&req).expect("missing name is ok");
2184 }
2185
2186 #[test]
2187 fn get_template_unknown_stack_returns_empty_body() {
2188 let svc = make_service();
2189 let mut params = HashMap::new();
2190 params.insert("StackName".to_string(), "ghost".to_string());
2191 let req = make_request("GetTemplate", params);
2192 svc.get_template(&req).expect("unknown is empty");
2193 }
2194
2195 #[test]
2196 fn update_stack_missing_name_errors() {
2197 let svc = make_service();
2198 let mut params = HashMap::new();
2199 params.insert("TemplateBody".to_string(), "{}".to_string());
2200 let req = make_request("UpdateStack", params);
2201 assert!(svc.update_stack(&req).is_err());
2202 }
2203
2204 #[test]
2205 fn update_stack_unknown_stack_returns_synthetic_id() {
2206 let svc = make_service();
2213 let mut params = HashMap::new();
2214 params.insert("StackName".to_string(), "ghost".to_string());
2215 params.insert(
2216 "TemplateBody".to_string(),
2217 r#"{"Resources":{}}"#.to_string(),
2218 );
2219 let req = make_request("UpdateStack", params);
2220 let resp = svc.update_stack(&req).expect("ghost update is synthetic");
2221 let b = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
2222 assert!(b.contains("UpdateStackResult"));
2223 }
2224
2225 #[test]
2226 fn create_stack_resolves_outputs_and_records_export() {
2227 let svc = make_service();
2228 let template = r#"{
2229 "Resources": {
2230 "Q": {"Type":"AWS::SQS::Queue","Properties":{"QueueName":"out-q"}}
2231 },
2232 "Outputs": {
2233 "QueueUrl": {
2234 "Value": {"Ref": "Q"},
2235 "Description": "Url",
2236 "Export": {"Name": "TheQueueUrl"}
2237 }
2238 }
2239 }"#;
2240 let mut params = HashMap::new();
2241 params.insert("StackName".to_string(), "outs".to_string());
2242 params.insert("TemplateBody".to_string(), template.to_string());
2243 let req = make_request("CreateStack", params);
2244 svc.create_stack(&req).expect("create stack");
2245
2246 let accounts = svc.state.read();
2247 let stack = accounts
2248 .get("123456789012")
2249 .unwrap()
2250 .stacks
2251 .get("outs")
2252 .unwrap();
2253 assert_eq!(stack.outputs.len(), 1);
2254 assert_eq!(stack.outputs[0].key, "QueueUrl");
2255 assert_eq!(stack.outputs[0].export_name.as_deref(), Some("TheQueueUrl"));
2256 assert!(!stack.outputs[0].value.is_empty());
2257 }
2258
2259 #[test]
2260 fn create_stack_rejects_duplicate_export_name() {
2261 let svc = make_service();
2262 let mk = |name: &str| {
2263 let template = format!(
2264 r#"{{
2265 "Resources": {{"Q":{{"Type":"AWS::SQS::Queue","Properties":{{"QueueName":"q-{name}"}}}}}},
2266 "Outputs": {{"QueueUrl":{{"Value":{{"Ref":"Q"}},"Export":{{"Name":"DupExport"}}}}}}
2267 }}"#
2268 );
2269 let mut params = HashMap::new();
2270 params.insert("StackName".to_string(), name.to_string());
2271 params.insert("TemplateBody".to_string(), template);
2272 make_request("CreateStack", params)
2273 };
2274 match svc.create_stack(&mk("first")) {
2275 Ok(_) => {}
2276 Err(e) => panic!("first stack: {e:?}"),
2277 }
2278 match svc.create_stack(&mk("second")) {
2279 Ok(_) => panic!("expected duplicate-export error"),
2280 Err(e) => assert!(
2281 format!("{e:?}").contains("already exported"),
2282 "expected duplicate-export error, got {e:?}"
2283 ),
2284 }
2285 }
2286
2287 #[test]
2288 fn import_value_resolves_against_other_stack_export() {
2289 let svc = make_service();
2290
2291 let producer_tpl = r#"{
2292 "Resources": {"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"prod-q"}}},
2293 "Outputs": {"Out":{"Value":{"Ref":"Q"},"Export":{"Name":"SharedQueueUrl"}}}
2294 }"#;
2295 let mut p = HashMap::new();
2296 p.insert("StackName".to_string(), "producer".to_string());
2297 p.insert("TemplateBody".to_string(), producer_tpl.to_string());
2298 svc.create_stack(&make_request("CreateStack", p))
2299 .expect("producer");
2300
2301 let consumer_tpl = r#"{
2302 "Resources": {"Q2":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"cons-q"}}},
2303 "Outputs": {"Imp":{"Value":{"Fn::ImportValue":"SharedQueueUrl"}}}
2304 }"#;
2305 let mut p = HashMap::new();
2306 p.insert("StackName".to_string(), "consumer".to_string());
2307 p.insert("TemplateBody".to_string(), consumer_tpl.to_string());
2308 svc.create_stack(&make_request("CreateStack", p))
2309 .expect("consumer");
2310
2311 let accounts = svc.state.read();
2312 let prod_url = accounts
2313 .get("123456789012")
2314 .unwrap()
2315 .stacks
2316 .get("producer")
2317 .unwrap()
2318 .outputs[0]
2319 .value
2320 .clone();
2321 let cons = accounts
2322 .get("123456789012")
2323 .unwrap()
2324 .stacks
2325 .get("consumer")
2326 .unwrap();
2327 assert_eq!(cons.outputs[0].value, prod_url);
2328 }
2329
2330 #[test]
2331 fn create_stack_records_export_in_state_registry() {
2332 let svc = make_service();
2333 let template = r#"{
2334 "Resources": {"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"reg-q"}}},
2335 "Outputs": {"Url":{"Value":{"Ref":"Q"},"Export":{"Name":"reg-url"}}}
2336 }"#;
2337 let mut params = HashMap::new();
2338 params.insert("StackName".to_string(), "reg".to_string());
2339 params.insert("TemplateBody".to_string(), template.to_string());
2340 svc.create_stack(&make_request("CreateStack", params))
2341 .expect("create");
2342
2343 let accounts = svc.state.read();
2344 let state = accounts.get("123456789012").unwrap();
2345 let export = state
2346 .exports
2347 .get("reg-url")
2348 .expect("export registered in state.exports");
2349 assert_eq!(export.exporting_stack_name, "reg");
2350 assert!(!export.value.is_empty());
2351 assert!(export.exporting_stack_id.contains("reg"));
2352 }
2353
2354 #[test]
2355 fn import_value_with_unknown_export_errors() {
2356 let svc = make_service();
2357 let consumer_tpl = r#"{
2358 "Resources": {"Q":{"Type":"AWS::SQS::Queue","Properties":{
2359 "QueueName": {"Fn::ImportValue":"missing-export"}
2360 }}}
2361 }"#;
2362 let mut p = HashMap::new();
2363 p.insert("StackName".to_string(), "bad-consumer".to_string());
2364 p.insert("TemplateBody".to_string(), consumer_tpl.to_string());
2365 match svc.create_stack(&make_request("CreateStack", p)) {
2366 Ok(_) => panic!("expected ValidationError for unknown export"),
2367 Err(e) => {
2368 let msg = format!("{e:?}");
2369 assert!(msg.contains("No export named missing-export"), "got {msg}");
2370 }
2371 }
2372 }
2373
2374 #[test]
2375 fn delete_stack_blocked_when_export_in_use_and_unblocked_after_consumer_delete() {
2376 let svc = make_service();
2377
2378 let producer_tpl = r#"{
2379 "Resources": {"Q":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"prod"}}},
2380 "Outputs": {"Out":{"Value":{"Ref":"Q"},"Export":{"Name":"my-arn"}}}
2381 }"#;
2382 let mut p = HashMap::new();
2383 p.insert("StackName".to_string(), "producer".to_string());
2384 p.insert("TemplateBody".to_string(), producer_tpl.to_string());
2385 svc.create_stack(&make_request("CreateStack", p))
2386 .expect("producer");
2387
2388 let consumer_tpl = r#"{
2389 "Resources": {"Q2":{"Type":"AWS::SQS::Queue","Properties":{
2390 "QueueName": "cons-q",
2391 "Tags": [{"Key":"k","Value":{"Fn::ImportValue":"my-arn"}}]
2392 }}}
2393 }"#;
2394 let mut p = HashMap::new();
2395 p.insert("StackName".to_string(), "consumer".to_string());
2396 p.insert("TemplateBody".to_string(), consumer_tpl.to_string());
2397 svc.create_stack(&make_request("CreateStack", p))
2398 .expect("consumer");
2399
2400 let mut p = HashMap::new();
2402 p.insert("StackName".to_string(), "producer".to_string());
2403 match svc.delete_stack(&make_request("DeleteStack", p)) {
2404 Ok(_) => panic!("delete must fail while imports exist"),
2405 Err(e) => {
2406 let msg = format!("{e:?}");
2407 assert!(msg.contains("Export my-arn cannot be deleted"), "got {msg}");
2408 }
2409 }
2410
2411 let mut p = HashMap::new();
2413 p.insert("StackName".to_string(), "consumer".to_string());
2414 svc.delete_stack(&make_request("DeleteStack", p))
2415 .expect("consumer delete");
2416
2417 let mut p = HashMap::new();
2419 p.insert("StackName".to_string(), "producer".to_string());
2420 svc.delete_stack(&make_request("DeleteStack", p))
2421 .expect("producer delete after consumer gone");
2422
2423 let accounts = svc.state.read();
2424 let state = accounts.get("123456789012").unwrap();
2425 assert!(state.exports.is_empty(), "exports cleared after delete");
2426 assert!(state.imports.is_empty(), "imports cleared after delete");
2427 }
2428}