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