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