1use chrono::Utc;
2use parking_lot::RwLock;
3use std::collections::BTreeMap;
4use std::sync::Arc;
5use uuid::Uuid;
6
7use crate::state::SharedCloudFormationState;
8use fakecloud_acm::{
9 CertificateOptions as AcmCertificateOptions, DomainValidation as AcmDomainValidation,
10 RenewalSummary as AcmRenewalSummary, SharedAcmState, StoredCertificate as AcmStoredCertificate,
11};
12use fakecloud_acmpca::SharedAcmPcaState;
13use fakecloud_apigateway::{
14 make_id as apigw_make_id, ApiKey as ApiGwApiKey, Authorizer as ApiGwAuthorizer,
15 Deployment as ApiGwDeployment, Integration as ApiGwIntegration, Method as ApiGwMethod,
16 Model as ApiGwModel, Resource as ApiGwResource, RestApi as ApiGwRestApi, SharedApiGatewayState,
17 Stage as ApiGwStage, UsagePlan as ApiGwUsagePlan,
18};
19use fakecloud_apigatewayv2::{
20 Authorizer as ApiGwV2Authorizer, CorsConfiguration as ApiGwV2CorsConfiguration,
21 Deployment as ApiGwV2Deployment, HttpApi as ApiGwV2HttpApi, Integration as ApiGwV2Integration,
22 JwtConfiguration as ApiGwV2JwtConfiguration, Route as ApiGwV2Route, SharedApiGatewayV2State,
23 Stage as ApiGwV2Stage,
24};
25use fakecloud_application_autoscaling::{
26 ScalableTarget as AppasScalableTarget, ScalingPolicy as AppasScalingPolicy,
27 SharedApplicationAutoScalingState as AppasState, SuspendedState as AppasSuspendedState,
28};
29use fakecloud_athena::{DataCatalog, NamedQuery, PreparedStatement, SharedAthenaState, WorkGroup};
30use fakecloud_aws::arn::Arn;
31use fakecloud_cloudfront::{
32 functions::{
33 CloudFrontOriginAccessIdentityConfig, FunctionConfig, KeyGroupConfig, KeyGroupItems,
34 PublicKeyConfig, StoredFunction, StoredKeyGroup, StoredOriginAccessIdentity,
35 StoredPublicKey,
36 },
37 model::{
38 DefaultCacheBehavior, DistributionConfig, Origin, OriginItems, Origins, ViewerCertificate,
39 },
40 policies::{
41 CachePolicyConfig, OriginAccessControlConfig, OriginRequestPolicyConfig,
42 OriginRequestPolicyCookiesConfig, OriginRequestPolicyHeadersConfig,
43 OriginRequestPolicyQueryStringsConfig, ResponseHeadersPolicyConfig, StoredCachePolicy,
44 StoredOriginAccessControl, StoredOriginRequestPolicy, StoredResponseHeadersPolicy,
45 },
46 SharedCloudFrontState, StoredDistribution,
47};
48use fakecloud_cloudwatch::{
49 AlarmMetricQuery, AlarmMetricStat, AlarmState, Dashboard, MetricAlarm, SharedCloudWatchState,
50};
51use fakecloud_cognito::{
52 default_schema_attributes, AccountRecoverySetting, AdminCreateUserConfig,
53 CognitoIdentityProvider, CustomDomainConfig, EmailConfiguration, IdentityPool,
54 IdentityPoolRoleAttachment, PasswordPolicy, PoolPolicies, RecoveryOption, SchemaAttribute,
55 SharedCognitoState, SignInPolicy, SmsConfiguration, UserPool, UserPoolClient, UserPoolDomain,
56};
57use fakecloud_core::delivery::DeliveryBus;
58use fakecloud_dynamodb::{
59 AttributeDefinition, DynamoTable, KeySchemaElement, OnDemandThroughput, ProvisionedThroughput,
60 SharedDynamoDbState,
61};
62use fakecloud_ecr::{Repository, SharedEcrState};
63use fakecloud_ecs::{
64 CapacityProvider as EcsCapacityProvider, Cluster as EcsCluster, Service as EcsService,
65 SharedEcsState, TagEntry as EcsTagEntry, TaskDefinition as EcsTaskDefinition,
66};
67use fakecloud_eks::SharedEksState;
68use fakecloud_elasticache::{
69 CacheCluster as EcCacheCluster, CacheParameterGroup, CacheSecurityGroup, CacheSubnetGroup,
70 ElastiCacheUser as EcUser, ElastiCacheUserGroup as EcUserGroup,
71 ReplicationGroup as EcReplicationGroup, SharedElastiCacheState,
72};
73use fakecloud_elbv2::{
74 Action as ElbAction, Listener, LoadBalancer, Rule as ElbRule, RuleCondition, SharedElbv2State,
75 Tag as ElbTag, TargetGroup, TargetGroupTuple,
76};
77use fakecloud_eventbridge::{
78 ApiDestination, Archive, Connection, Endpoint, EventBus, EventRule, SharedEventBridgeState,
79};
80use fakecloud_firehose::{DeliveryStream, S3Destination};
81use fakecloud_iam::{
82 IamAccessKey, IamGroup, IamInstanceProfile, IamPolicy, IamRole, IamUser, OidcProvider,
83 PolicyVersion, SamlProvider, SharedIamState, Tag, VirtualMfaDevice,
84};
85use fakecloud_kinesis::{build_stream_shards, KinesisConsumer, KinesisStream, SharedKinesisState};
86use fakecloud_kms::provisioner as kms_provisioner;
87use fakecloud_kms::SharedKmsState;
88use fakecloud_lambda::{
89 AttachedLayer, EventSourceMapping, FunctionAlias, FunctionUrlConfig, Layer, LayerVersion,
90 SharedLambdaState,
91};
92use fakecloud_logs::{
93 Delivery, DeliveryDestination, DeliverySource, Destination, LogStream, MetricFilter,
94 MetricTransformation, QueryDefinition, ResourcePolicy, SharedLogsState, SubscriptionFilter,
95};
96use fakecloud_organizations::{
97 OrganizationState, OrganizationalUnit, Policy as OrgPolicy, SharedOrganizationsState,
98 POLICY_TYPE_SCP,
99};
100use fakecloud_persistence::{BucketSubresource, S3Store};
101use fakecloud_rds::{DbInstance, DbParameterGroup, DbSubnetGroup, RdsTag, SharedRdsState};
102use fakecloud_route53::{
103 model::{HealthCheckConfig, HostedZoneFeatures, ResourceRecordSet},
104 SharedRoute53State, StoredHealthCheck, StoredHostedZone,
105};
106use fakecloud_s3::persistence::bucket_meta_snapshot;
107use fakecloud_s3::{S3Bucket, SharedS3State};
108use fakecloud_secretsmanager::{RotationRules, Secret, SecretVersion, SharedSecretsManagerState};
109use fakecloud_servicediscovery::SharedServiceDiscoveryState;
110use fakecloud_ses::{
111 ConfigurationSet as SesConfigurationSet, ContactList as SesContactList,
112 DedicatedIpPool as SesDedicatedIpPool, EmailIdentity as SesEmailIdentity,
113 EmailTemplate as SesEmailTemplate, EventDestination as SesEventDestination,
114 IpFilter as SesIpFilter, ReceiptAction as SesReceiptAction, ReceiptFilter as SesReceiptFilter,
115 ReceiptRule as SesReceiptRule, ReceiptRuleSet as SesReceiptRuleSet, SharedSesState,
116};
117use fakecloud_sns::{SharedSnsState, SnsSubscription, SnsTopic};
118use fakecloud_sqs::{SharedSqsState, SqsQueue};
119use fakecloud_ssm::{SharedSsmState, SsmParameter};
120use fakecloud_stepfunctions::{
121 Activity as SfnActivity, AliasRoute, SharedStepFunctionsState, StateMachine, StateMachineAlias,
122 StateMachineStatus, StateMachineType, StateMachineVersion,
123};
124use fakecloud_wafv2::{IpSet, RegexPatternSet, RuleGroup, SharedWafv2State, WebAcl};
125
126use crate::state::StackResource;
127use crate::template::ResourceDefinition;
128
129fn parse_iam_tags(value: Option<&serde_json::Value>) -> Vec<Tag> {
133 let Some(arr) = value.and_then(|v| v.as_array()) else {
134 return Vec::new();
135 };
136 arr.iter()
137 .filter_map(|t| {
138 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
139 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
140 Some(Tag { key, value })
141 })
142 .collect()
143}
144
145fn parse_elb_tags(value: Option<&serde_json::Value>) -> Vec<ElbTag> {
148 let Some(arr) = value.and_then(|v| v.as_array()) else {
149 return Vec::new();
150 };
151 arr.iter()
152 .filter_map(|t| {
153 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
154 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
155 Some(ElbTag { key, value })
156 })
157 .collect()
158}
159
160fn parse_elb_actions(value: Option<&serde_json::Value>) -> Vec<ElbAction> {
164 let Some(arr) = value.and_then(|v| v.as_array()) else {
165 return Vec::new();
166 };
167 arr.iter()
168 .map(|a| {
169 let action_type = a
170 .get("Type")
171 .and_then(|v| v.as_str())
172 .unwrap_or("forward")
173 .to_string();
174 let target_group_arn = a
175 .get("TargetGroupArn")
176 .and_then(|v| v.as_str())
177 .map(|s| s.to_string());
178 let order = a.get("Order").and_then(|v| v.as_i64()).map(|n| n as i32);
179 let redirect = a
180 .get("RedirectConfig")
181 .map(|r| fakecloud_elbv2::RedirectConfig {
182 protocol: r
183 .get("Protocol")
184 .and_then(|v| v.as_str())
185 .map(|s| s.to_string()),
186 port: r
187 .get("Port")
188 .and_then(|v| v.as_str())
189 .map(|s| s.to_string()),
190 host: r
191 .get("Host")
192 .and_then(|v| v.as_str())
193 .map(|s| s.to_string()),
194 path: r
195 .get("Path")
196 .and_then(|v| v.as_str())
197 .map(|s| s.to_string()),
198 query: r
199 .get("Query")
200 .and_then(|v| v.as_str())
201 .map(|s| s.to_string()),
202 status_code: r
203 .get("StatusCode")
204 .and_then(|v| v.as_str())
205 .unwrap_or("HTTP_302")
206 .to_string(),
207 });
208 let fixed_response =
209 a.get("FixedResponseConfig")
210 .map(|f| fakecloud_elbv2::FixedResponseConfig {
211 message_body: f
212 .get("MessageBody")
213 .and_then(|v| v.as_str())
214 .map(|s| s.to_string()),
215 status_code: f
216 .get("StatusCode")
217 .and_then(|v| v.as_str())
218 .unwrap_or("200")
219 .to_string(),
220 content_type: f
221 .get("ContentType")
222 .and_then(|v| v.as_str())
223 .map(|s| s.to_string()),
224 });
225 let forward = a.get("ForwardConfig").map(|f| {
226 let target_groups: Vec<TargetGroupTuple> = f
227 .get("TargetGroups")
228 .and_then(|v| v.as_array())
229 .map(|arr| {
230 arr.iter()
231 .filter_map(|t| {
232 let target_group_arn = t
233 .get("TargetGroupArn")
234 .and_then(|v| v.as_str())?
235 .to_string();
236 let weight =
237 t.get("Weight").and_then(|v| v.as_i64()).map(|n| n as i32);
238 Some(TargetGroupTuple {
239 target_group_arn,
240 weight,
241 })
242 })
243 .collect()
244 })
245 .unwrap_or_default();
246 fakecloud_elbv2::ForwardConfig {
247 target_groups,
248 stickiness: None,
249 }
250 });
251 ElbAction {
252 action_type,
253 target_group_arn,
254 order,
255 redirect,
256 fixed_response,
257 forward,
258 authenticate_cognito: None,
259 authenticate_oidc: None,
260 }
261 })
262 .collect()
263}
264
265fn parse_elb_rule_conditions(value: Option<&serde_json::Value>) -> Vec<RuleCondition> {
266 let Some(arr) = value.and_then(|v| v.as_array()) else {
267 return Vec::new();
268 };
269 arr.iter()
270 .map(|c| {
271 let field = c
272 .get("Field")
273 .and_then(|v| v.as_str())
274 .unwrap_or("")
275 .to_string();
276 let values: Vec<String> = c
277 .get("Values")
278 .and_then(|v| v.as_array())
279 .map(|arr| {
280 arr.iter()
281 .filter_map(|s| s.as_str().map(|s| s.to_string()))
282 .collect()
283 })
284 .unwrap_or_default();
285 let host_header_values: Vec<String> = c
286 .get("HostHeaderConfig")
287 .and_then(|v| v.get("Values"))
288 .and_then(|v| v.as_array())
289 .map(|arr| {
290 arr.iter()
291 .filter_map(|s| s.as_str().map(|s| s.to_string()))
292 .collect()
293 })
294 .unwrap_or_default();
295 RuleCondition {
296 field,
297 values,
298 host_header_values,
299 path_pattern_values: Vec::new(),
300 http_header_name: None,
301 http_header_values: Vec::new(),
302 query_string_values: Vec::new(),
303 http_request_method_values: Vec::new(),
304 source_ip_values: Vec::new(),
305 }
306 })
307 .collect()
308}
309
310fn parse_key_policy(props: &serde_json::Value) -> Option<String> {
315 match props.get("KeyPolicy") {
316 Some(v) if v.is_string() => Some(v.as_str().unwrap_or("").to_string()),
317 Some(v) => Some(serde_json::to_string(v).unwrap_or_default()),
318 None => None,
319 }
320}
321
322fn parse_tag_list(props: &serde_json::Value) -> BTreeMap<String, String> {
325 let mut tags: BTreeMap<String, String> = BTreeMap::new();
326 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
327 for t in arr {
328 if let (Some(k), Some(v)) = (
329 t.get("Key").and_then(|x| x.as_str()),
330 t.get("Value").and_then(|x| x.as_str()),
331 ) {
332 tags.insert(k.to_string(), v.to_string());
333 }
334 }
335 }
336 tags
337}
338
339fn parse_kms_key_input(props: &serde_json::Value) -> kms_provisioner::KeyCreationInput {
349 kms_provisioner::KeyCreationInput {
350 description: props
351 .get("Description")
352 .and_then(|v| v.as_str())
353 .unwrap_or("")
354 .to_string(),
355 key_usage: props
356 .get("KeyUsage")
357 .and_then(|v| v.as_str())
358 .unwrap_or("ENCRYPT_DECRYPT")
359 .to_string(),
360 key_spec: props
361 .get("KeySpec")
362 .and_then(|v| v.as_str())
363 .unwrap_or("SYMMETRIC_DEFAULT")
364 .to_string(),
365 origin: props
366 .get("Origin")
367 .and_then(|v| v.as_str())
368 .unwrap_or("AWS_KMS")
369 .to_string(),
370 enabled: props
371 .get("Enabled")
372 .and_then(|v| v.as_bool())
373 .unwrap_or(true),
374 multi_region: props
375 .get("MultiRegion")
376 .and_then(|v| v.as_bool())
377 .unwrap_or(false),
378 key_rotation_enabled: props
379 .get("EnableKeyRotation")
380 .and_then(|v| v.as_bool())
381 .unwrap_or(false),
382 policy: parse_key_policy(props),
383 tags: parse_tag_list(props),
384 }
385}
386
387fn parse_log_group_name(input: &str) -> String {
391 if let Some(rest) = input.strip_prefix("arn:aws:logs:") {
392 if let Some(after) = rest.split(":log-group:").nth(1) {
393 return after.trim_end_matches(":*").to_string();
395 }
396 }
397 input.to_string()
398}
399
400fn parse_lambda_function_name(input: &str) -> String {
408 if let Some(rest) = input.strip_prefix("arn:aws:lambda:") {
410 if let Some(after) = rest.split(":function:").nth(1) {
411 return after.split(':').next().unwrap_or(after).to_string();
412 }
413 }
414 if let Some(after) = input.split(":function:").nth(1) {
416 return after.split(':').next().unwrap_or(after).to_string();
417 }
418 input.split(':').next().unwrap_or(input).to_string()
420}
421
422fn alias_state_key(physical_id: &str) -> String {
428 if let Some(rest) = physical_id.strip_prefix("arn:aws:lambda:") {
429 if let Some(after) = rest.split(":function:").nth(1) {
430 return after.to_string();
431 }
432 }
433 physical_id.to_string()
434}
435
436struct LambdaFunctionProps {
440 runtime: String,
441 role: String,
442 handler: String,
443 description: String,
444 timeout: i64,
445 memory_size: i64,
446 package_type: String,
447 tags: BTreeMap<String, String>,
448 environment: BTreeMap<String, String>,
449 architectures: Vec<String>,
450 code_zip: Option<Vec<u8>>,
454 s3_bucket: Option<String>,
455 s3_key: Option<String>,
456 image_uri: Option<String>,
457 layers: Vec<String>,
458 tracing_mode: Option<String>,
459 kms_key_arn: Option<String>,
460 ephemeral_storage_size: Option<i64>,
461 vpc_config: Option<serde_json::Value>,
462 snap_start: Option<serde_json::Value>,
463 dead_letter_config_arn: Option<String>,
464 file_system_configs: Vec<serde_json::Value>,
465 logging_config: Option<serde_json::Value>,
466}
467
468fn parse_lambda_function_props(props: &serde_json::Value) -> Result<LambdaFunctionProps, String> {
473 let runtime = props
474 .get("Runtime")
475 .and_then(|v| v.as_str())
476 .unwrap_or("python3.12")
477 .to_string();
478 let role = props
479 .get("Role")
480 .and_then(|v| v.as_str())
481 .unwrap_or_default()
482 .to_string();
483 let handler = props
484 .get("Handler")
485 .and_then(|v| v.as_str())
486 .unwrap_or("index.handler")
487 .to_string();
488 let description = props
489 .get("Description")
490 .and_then(|v| v.as_str())
491 .unwrap_or_default()
492 .to_string();
493 let timeout = props.get("Timeout").and_then(|v| v.as_i64()).unwrap_or(3);
494 let memory_size = props
495 .get("MemorySize")
496 .and_then(|v| v.as_i64())
497 .unwrap_or(128);
498 let architectures = props
499 .get("Architectures")
500 .and_then(|v| v.as_array())
501 .map(|a| {
502 a.iter()
503 .filter_map(|v| v.as_str().map(|s| s.to_string()))
504 .collect::<Vec<_>>()
505 })
506 .unwrap_or_else(|| vec!["x86_64".to_string()]);
507 let package_type = props
508 .get("PackageType")
509 .and_then(|v| v.as_str())
510 .unwrap_or("Zip")
511 .to_string();
512 let environment = props
513 .get("Environment")
514 .and_then(|v| v.get("Variables"))
515 .and_then(|v| v.as_object())
516 .map(|o| {
517 o.iter()
518 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
519 .collect::<BTreeMap<String, String>>()
520 })
521 .unwrap_or_default();
522
523 let tags: BTreeMap<String, String> = props
526 .get("Tags")
527 .and_then(|v| v.as_array())
528 .map(|arr| {
529 arr.iter()
530 .filter_map(|t| {
531 let k = t.get("Key").and_then(|v| v.as_str())?.to_string();
532 let v = t.get("Value").and_then(|v| v.as_str())?.to_string();
533 Some((k, v))
534 })
535 .collect()
536 })
537 .unwrap_or_default();
538
539 let code = props.get("Code");
540 let code_zip = code
546 .and_then(|c| c.get("ZipFile"))
547 .and_then(|v| v.as_str())
548 .map(|s| s.as_bytes().to_vec());
549 let s3_bucket = code
550 .and_then(|c| c.get("S3Bucket"))
551 .and_then(|v| v.as_str())
552 .map(|s| s.to_string());
553 let s3_key = code
554 .and_then(|c| c.get("S3Key"))
555 .and_then(|v| v.as_str())
556 .map(|s| s.to_string());
557 let image_uri = if package_type == "Image" {
561 code.and_then(|c| c.get("ImageUri"))
562 .and_then(|v| v.as_str())
563 .map(|s| s.to_string())
564 } else {
565 None
566 };
567 if package_type == "Image" && image_uri.is_none() {
568 return Err("Code.ImageUri is required when PackageType is Image".to_string());
569 }
570
571 let layers: Vec<String> = props
572 .get("Layers")
573 .and_then(|v| v.as_array())
574 .map(|arr| {
575 arr.iter()
576 .filter_map(|v| v.as_str().map(String::from))
577 .collect()
578 })
579 .unwrap_or_default();
580
581 let tracing_mode = props
582 .get("TracingConfig")
583 .and_then(|v| v.get("Mode"))
584 .and_then(|v| v.as_str())
585 .map(String::from);
586 let kms_key_arn = props
587 .get("KmsKeyArn")
588 .and_then(|v| v.as_str())
589 .map(String::from);
590 let ephemeral_storage_size = props
591 .get("EphemeralStorage")
592 .and_then(|v| v.get("Size"))
593 .and_then(|v| v.as_i64());
594 let vpc_config = props.get("VpcConfig").filter(|v| v.is_object()).cloned();
595 let snap_start = props.get("SnapStart").filter(|v| v.is_object()).cloned();
596 let dead_letter_config_arn = props
597 .get("DeadLetterConfig")
598 .and_then(|v| v.get("TargetArn"))
599 .and_then(|v| v.as_str())
600 .map(String::from);
601 let file_system_configs = props
602 .get("FileSystemConfigs")
603 .and_then(|v| v.as_array())
604 .cloned()
605 .unwrap_or_default();
606 let logging_config = props
607 .get("LoggingConfig")
608 .filter(|v| v.is_object())
609 .cloned();
610
611 Ok(LambdaFunctionProps {
612 runtime,
613 role,
614 handler,
615 description,
616 timeout,
617 memory_size,
618 package_type,
619 tags,
620 environment,
621 architectures,
622 code_zip,
623 s3_bucket,
624 s3_key,
625 image_uri,
626 layers,
627 tracing_mode,
628 kms_key_arn,
629 ephemeral_storage_size,
630 vpc_config,
631 snap_start,
632 dead_letter_config_arn,
633 file_system_configs,
634 logging_config,
635 })
636}
637
638struct LambdaEventSourceMappingProps {
643 event_source_arn: String,
644 batch_size: i64,
645 enabled: bool,
646 starting_position: Option<String>,
647 starting_position_timestamp: Option<f64>,
648 parallelization_factor: Option<i64>,
649 maximum_batching_window_in_seconds: Option<i64>,
650 function_response_types: Vec<String>,
651 filter_patterns: Vec<String>,
652 kms_key_arn: Option<String>,
653 metrics_config: Option<serde_json::Value>,
654 destination_config: Option<serde_json::Value>,
655 maximum_retry_attempts: Option<i64>,
656 maximum_record_age_in_seconds: Option<i64>,
657 bisect_batch_on_function_error: Option<bool>,
658 tumbling_window_in_seconds: Option<i64>,
659 topics: Vec<String>,
660 queues: Vec<String>,
661}
662
663fn parse_lambda_event_source_mapping_props(
667 props: &serde_json::Value,
668) -> Result<LambdaEventSourceMappingProps, String> {
669 let event_source_arn = props
670 .get("EventSourceArn")
671 .and_then(|v| v.as_str())
672 .unwrap_or_default()
673 .to_string();
674 let batch_size = props
675 .get("BatchSize")
676 .and_then(|v| v.as_i64())
677 .unwrap_or(10);
678 let enabled = props
679 .get("Enabled")
680 .and_then(|v| v.as_bool())
681 .unwrap_or(true);
682 let starting_position = props
683 .get("StartingPosition")
684 .and_then(|v| v.as_str())
685 .map(|s| s.to_string());
686 let starting_position_timestamp = props
687 .get("StartingPositionTimestamp")
688 .and_then(|v| v.as_f64());
689 let parallelization_factor = props.get("ParallelizationFactor").and_then(|v| v.as_i64());
690 let maximum_batching_window_in_seconds = props
691 .get("MaximumBatchingWindowInSeconds")
692 .and_then(|v| v.as_i64());
693 let function_response_types: Vec<String> = props
694 .get("FunctionResponseTypes")
695 .and_then(|v| v.as_array())
696 .map(|arr| {
697 arr.iter()
698 .filter_map(|v| v.as_str().map(|s| s.to_string()))
699 .collect()
700 })
701 .unwrap_or_default();
702 let filter_patterns: Vec<String> = props
703 .get("FilterCriteria")
704 .and_then(|v| v.get("Filters"))
705 .and_then(|v| v.as_array())
706 .map(|arr| {
707 arr.iter()
708 .filter_map(|f| {
709 f.get("Pattern")
710 .and_then(|p| p.as_str())
711 .map(|s| s.to_string())
712 })
713 .collect()
714 })
715 .unwrap_or_default();
716 let kms_key_arn = props
717 .get("KmsKeyArn")
718 .and_then(|v| v.as_str())
719 .map(|s| s.to_string());
720 let metrics_config = props
721 .get("MetricsConfig")
722 .filter(|v| v.is_object())
723 .cloned();
724 let destination_config = props
725 .get("DestinationConfig")
726 .filter(|v| v.is_object())
727 .cloned();
728 let maximum_retry_attempts = props.get("MaximumRetryAttempts").and_then(|v| v.as_i64());
729 let maximum_record_age_in_seconds = props
730 .get("MaximumRecordAgeInSeconds")
731 .and_then(|v| v.as_i64());
732 let bisect_batch_on_function_error = props
733 .get("BisectBatchOnFunctionError")
734 .and_then(|v| v.as_bool());
735 let tumbling_window_in_seconds = props
736 .get("TumblingWindowInSeconds")
737 .and_then(|v| v.as_i64());
738 let topics: Vec<String> = props
739 .get("Topics")
740 .and_then(|v| v.as_array())
741 .map(|arr| {
742 arr.iter()
743 .filter_map(|v| v.as_str().map(|s| s.to_string()))
744 .collect()
745 })
746 .unwrap_or_default();
747 let queues: Vec<String> = props
748 .get("Queues")
749 .and_then(|v| v.as_array())
750 .map(|arr| {
751 arr.iter()
752 .filter_map(|v| v.as_str().map(|s| s.to_string()))
753 .collect()
754 })
755 .unwrap_or_default();
756
757 Ok(LambdaEventSourceMappingProps {
758 event_source_arn,
759 batch_size,
760 enabled,
761 starting_position,
762 starting_position_timestamp,
763 parallelization_factor,
764 maximum_batching_window_in_seconds,
765 function_response_types,
766 filter_patterns,
767 kms_key_arn,
768 metrics_config,
769 destination_config,
770 maximum_retry_attempts,
771 maximum_record_age_in_seconds,
772 bisect_batch_on_function_error,
773 tumbling_window_in_seconds,
774 topics,
775 queues,
776 })
777}
778
779fn sha256_b64(bytes: &[u8]) -> String {
782 use sha2::Digest;
783 let hash = sha2::Sha256::digest(bytes);
784 base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash)
785}
786
787fn layer_code_size(
792 accounts: &fakecloud_core::multi_account::MultiAccountState<fakecloud_lambda::LambdaState>,
793 arn: &str,
794) -> i64 {
795 let Some(rest) = arn.strip_prefix("arn:aws:lambda:") else {
797 return 0;
798 };
799 let mut parts = rest.split(':');
800 let _region = parts.next();
801 let Some(account) = parts.next() else {
802 return 0;
803 };
804 if parts.next() != Some("layer") {
805 return 0;
806 }
807 let Some(name) = parts.next() else {
808 return 0;
809 };
810 let Some(ver_str) = parts.next() else {
811 return 0;
812 };
813 let Ok(ver) = ver_str.parse::<i64>() else {
814 return 0;
815 };
816 accounts
817 .get(account)
818 .and_then(|s| s.layers.get(name))
819 .and_then(|l| l.versions.iter().find(|v| v.version == ver))
820 .map(|v| v.code_size)
821 .unwrap_or(0)
822}
823
824pub struct ProvisionResult {
827 pub physical_id: String,
828 pub attributes: BTreeMap<String, String>,
829}
830
831impl ProvisionResult {
832 pub fn new(physical_id: impl Into<String>) -> Self {
833 Self {
834 physical_id: physical_id.into(),
835 attributes: BTreeMap::new(),
836 }
837 }
838
839 pub fn with(mut self, key: &str, value: impl Into<String>) -> Self {
840 self.attributes.insert(key.to_string(), value.into());
841 self
842 }
843
844 pub fn merge_attributes(mut self, other: BTreeMap<String, String>) -> Self {
849 for (k, v) in other {
850 self.attributes.insert(k, v);
851 }
852 self
853 }
854}
855
856fn policy_document_string(props: &serde_json::Value) -> Result<String, String> {
860 match props.get("PolicyDocument") {
861 Some(serde_json::Value::String(s)) => Ok(s.clone()),
862 Some(other) => Ok(other.to_string()),
863 None => Err("PolicyDocument is required".to_string()),
864 }
865}
866
867pub struct ResourceProvisioner {
869 pub sqs_state: SharedSqsState,
870 pub sns_state: SharedSnsState,
871 pub ssm_state: SharedSsmState,
872 pub iam_state: SharedIamState,
873 pub s3_state: SharedS3State,
874 pub eventbridge_state: SharedEventBridgeState,
875 pub dynamodb_state: SharedDynamoDbState,
876 pub logs_state: SharedLogsState,
877 pub lambda_state: SharedLambdaState,
878 pub secretsmanager_state: SharedSecretsManagerState,
879 pub kinesis_state: SharedKinesisState,
880 pub kms_state: SharedKmsState,
881 pub ecr_state: SharedEcrState,
882 pub cloudwatch_state: SharedCloudWatchState,
883 pub elbv2_state: SharedElbv2State,
884 pub organizations_state: SharedOrganizationsState,
885 pub cognito_state: SharedCognitoState,
886 pub rds_state: SharedRdsState,
887 pub ec2_state: fakecloud_ec2::SharedEc2State,
888 pub autoscaling_state: fakecloud_autoscaling::SharedAutoScalingState,
889 pub batch_state: fakecloud_batch::SharedBatchState,
890 pub pipes_state: fakecloud_pipes::SharedPipesState,
891 pub ecs_state: SharedEcsState,
892 pub acm_state: SharedAcmState,
893 pub acmpca_state: SharedAcmPcaState,
894 pub config_state: fakecloud_config::SharedConfigState,
895 pub route53resolver_state: fakecloud_route53resolver::SharedRoute53ResolverState,
896 pub elasticache_state: SharedElastiCacheState,
897 pub route53_state: SharedRoute53State,
898 pub cloudfront_state: SharedCloudFrontState,
899 pub stepfunctions_state: SharedStepFunctionsState,
900 pub wafv2_state: SharedWafv2State,
901 pub apigateway_state: SharedApiGatewayState,
902 pub apigatewayv2_state: SharedApiGatewayV2State,
903 pub ses_state: SharedSesState,
904 pub app_autoscaling_state: AppasState,
905 pub athena_state: SharedAthenaState,
906 pub firehose_state: fakecloud_firehose::SharedFirehoseState,
907 pub glue_state: fakecloud_glue::SharedGlueState,
908 pub eks_state: SharedEksState,
909 pub servicediscovery_state: SharedServiceDiscoveryState,
910 pub codeartifact_state: fakecloud_codeartifact::SharedCodeArtifactState,
911 pub codecommit_state: fakecloud_codecommit::SharedCodeCommitState,
912 pub efs_state: fakecloud_efs::SharedEfsState,
913 pub elasticbeanstalk_state: fakecloud_elasticbeanstalk::SharedEbState,
914 pub mq_state: fakecloud_mq::SharedMqState,
915 pub kafka_state: fakecloud_kafka::SharedKafkaState,
916 pub ka2_state: fakecloud_kinesisanalyticsv2::SharedKa2State,
917 pub cloudformation_state: SharedCloudFormationState,
918 pub delivery: Arc<DeliveryBus>,
919 pub lambda_runtime: Option<Arc<fakecloud_lambda::runtime::ContainerRuntime>>,
923 pub rds_runtime: Option<Arc<fakecloud_rds::runtime::RdsRuntime>>,
927 pub ec2_runtime: Option<Arc<fakecloud_ec2::runtime::Ec2Runtime>>,
928 pub ecs_runtime: Option<Arc<fakecloud_ecs::runtime::EcsRuntime>>,
929 pub elasticache_runtime: Option<Arc<fakecloud_elasticache::runtime::ElastiCacheRuntime>>,
930 pub mq_runtime: Option<Arc<fakecloud_mq::MqRuntime>>,
931 pub kafka_runtime: Option<Arc<fakecloud_kafka::KafkaRuntime>>,
932 pub pending_container_spawns: Arc<parking_lot::Mutex<Vec<ContainerSpawnIntent>>>,
939 pub pending_container_teardowns: Arc<parking_lot::Mutex<Vec<ContainerTeardownIntent>>>,
948 pub pending_custom_invokes: Arc<parking_lot::Mutex<Vec<CustomInvokeIntent>>>,
958 pub defer_custom_invokes: bool,
965 pub s3_store: Arc<dyn S3Store>,
970 pub account_id: String,
971 pub region: String,
972 pub stack_id: String,
973 pub strict_unknown_types: bool,
982}
983
984#[derive(Debug, Clone)]
989pub enum ContainerSpawnIntent {
990 RdsInstance { identifier: String },
993 AsgInstances { group_name: String },
997 Ec2Instance { instance_id: String },
1001 ElastiCacheCluster { cache_cluster_id: String },
1005 ElastiCacheReplicationGroup { replication_group_id: String },
1009 EcsServiceTasks {
1014 cluster_name: String,
1015 service_name: String,
1016 },
1017 MqBroker { broker_id: String },
1021 MskCluster { cluster_arn: String },
1025}
1026
1027#[derive(Debug, Clone)]
1033pub enum ContainerTeardownIntent {
1034 RdsInstance { identifier: String },
1037 ElastiCacheCluster { cache_cluster_id: String },
1040 ElastiCacheReplicationGroup { replication_group_id: String },
1043 EcsService {
1046 cluster_name: String,
1047 service_name: String,
1048 },
1049 AsgInstances { instance_ids: Vec<String> },
1052 Ec2Instance { instance_id: String },
1055 MqBroker { broker_id: String },
1057 MskCluster { cluster_arn: String },
1059}
1060
1061#[derive(Debug, Clone)]
1066pub struct CustomInvokeIntent {
1067 pub service_token: String,
1068 pub payload: String,
1069}
1070
1071mod acm;
1072mod acmpca;
1073mod apigw;
1074mod apigwv2;
1075mod appautoscaling;
1076mod athena;
1077mod autoscaling;
1078mod batch;
1079mod cloudformation;
1080mod cloudfront;
1081mod cloudwatch;
1082mod codeartifact;
1083mod codecommit;
1084mod cognito;
1085mod config;
1086mod cwlogs;
1087mod dynamodb;
1088mod ec2;
1089mod ecr;
1090mod ecs;
1091mod efs;
1092mod eks;
1093mod elasticache;
1094mod elasticbeanstalk;
1095mod elbv2;
1096mod eventbridge;
1097mod firehose;
1098mod glue;
1099mod iam;
1100mod kafka;
1101mod kinesis;
1102mod kinesisanalyticsv2;
1103mod kms;
1104mod lambda;
1105mod logs;
1106mod mq;
1107mod organizations;
1108mod pipes;
1109mod rds;
1110mod route;
1111mod route53resolver;
1112mod s3;
1113mod secrets;
1114mod servicediscovery;
1115mod ses;
1116mod sns;
1117mod sqs;
1118mod ssm;
1119mod stepfunctions;
1120mod wafv2;
1121
1122impl ResourceProvisioner {
1123 pub fn create_resource(&self, resource: &ResourceDefinition) -> Result<StackResource, String> {
1125 let result = match resource.resource_type.as_str() {
1126 "AWS::SQS::Queue" => self.create_sqs_queue(resource),
1127 "AWS::SQS::QueuePolicy" => self.create_sqs_queue_policy(resource),
1128 "AWS::SNS::Topic" => self.create_sns_topic(resource),
1129 "AWS::SNS::TopicPolicy" => self.create_sns_topic_policy(resource),
1130 "AWS::SNS::Subscription" => self.create_sns_subscription(resource),
1131 "AWS::SSM::Parameter" => self.create_ssm_parameter(resource),
1132 "AWS::IAM::Role" => self.create_iam_role(resource),
1133 "AWS::IAM::Policy" => self.create_iam_policy(resource),
1134 "AWS::IAM::User" => self.create_iam_user(resource),
1135 "AWS::IAM::Group" => self.create_iam_group(resource),
1136 "AWS::IAM::ManagedPolicy" => self.create_iam_managed_policy(resource),
1137 "AWS::IAM::UserToGroupAddition" => self.create_iam_user_to_group_addition(resource),
1138 "AWS::IAM::AccessKey" => self.create_iam_access_key(resource),
1139 "AWS::IAM::InstanceProfile" => self.create_iam_instance_profile(resource),
1140 "AWS::IAM::OIDCProvider" => self.create_iam_oidc_provider(resource),
1141 "AWS::IAM::SAMLProvider" => self.create_iam_saml_provider(resource),
1142 "AWS::IAM::ServiceLinkedRole" => self.create_iam_service_linked_role(resource),
1143 "AWS::IAM::VirtualMFADevice" => self.create_iam_virtual_mfa_device(resource),
1144 "AWS::S3::Bucket" => self.create_s3_bucket(resource),
1145 "AWS::S3::BucketPolicy" => self.create_s3_bucket_policy(resource),
1146 "AWS::Events::Rule" => self.create_eventbridge_rule(resource),
1147 "AWS::Events::Connection" => self.create_eventbridge_connection(resource),
1148 "AWS::Events::ApiDestination" => self.create_eventbridge_api_destination(resource),
1149 "AWS::Events::Archive" => self.create_eventbridge_archive(resource),
1150 "AWS::Events::EventBus" => self.create_eventbridge_event_bus(resource),
1151 "AWS::Events::EventBusPolicy" => self.create_eventbridge_event_bus_policy(resource),
1152 "AWS::Events::Endpoint" => self.create_eventbridge_endpoint(resource),
1153 "AWS::DynamoDB::Table" => self.create_dynamodb_table(resource),
1154 "AWS::Logs::LogGroup" => self.create_log_group(resource),
1155 "AWS::Logs::LogStream" => self.create_log_stream(resource),
1156 "AWS::Logs::MetricFilter" => self.create_metric_filter(resource),
1157 "AWS::Logs::SubscriptionFilter" => self.create_subscription_filter(resource),
1158 "AWS::Logs::Destination" => self.create_logs_destination(resource),
1159 "AWS::Logs::ResourcePolicy" => self.create_logs_resource_policy(resource),
1160 "AWS::Logs::QueryDefinition" => self.create_logs_query_definition(resource),
1161 "AWS::Logs::Delivery" => self.create_logs_delivery(resource),
1162 "AWS::Logs::DeliveryDestination" => self.create_logs_delivery_destination(resource),
1163 "AWS::Logs::DeliverySource" => self.create_logs_delivery_source(resource),
1164 "AWS::Lambda::Function" => self.create_lambda_function(resource),
1165 "AWS::Lambda::Permission" => self.create_lambda_permission(resource),
1166 "AWS::Lambda::EventSourceMapping" => self.create_lambda_event_source_mapping(resource),
1167 "AWS::Lambda::LayerVersion" => self.create_lambda_layer_version(resource),
1168 "AWS::Lambda::Url" => self.create_lambda_url(resource),
1169 "AWS::Lambda::Alias" => self.create_lambda_alias(resource),
1170 "AWS::Lambda::Version" => self.create_lambda_version(resource),
1171 "AWS::SecretsManager::Secret" => self.create_secrets_manager_secret(resource),
1172 "AWS::Kinesis::Stream" => self.create_kinesis_stream(resource),
1173 "AWS::Kinesis::StreamConsumer" => self.create_kinesis_stream_consumer(resource),
1174 "AWS::KMS::Key" => self.create_kms_key(resource),
1175 "AWS::KMS::Alias" => self.create_kms_alias(resource),
1176 "AWS::KMS::ReplicaKey" => self.create_kms_replica_key(resource),
1177 "AWS::ECR::Repository" => self.create_ecr_repository(resource),
1178 "AWS::ECR::RepositoryPolicy" => self.create_ecr_repository_policy(resource),
1179 "AWS::ECR::LifecyclePolicy" => self.create_ecr_lifecycle_policy(resource),
1180 "AWS::ECR::RegistryPolicy" => self.create_ecr_registry_policy(resource),
1181 "AWS::ECR::ReplicationConfiguration" => {
1182 self.create_ecr_replication_configuration(resource)
1183 }
1184 "AWS::ECR::RegistryScanningConfiguration" => {
1185 self.create_ecr_registry_scanning_configuration(resource)
1186 }
1187 "AWS::ECR::PullThroughCacheRule" => self.create_ecr_pull_through_cache_rule(resource),
1188 "AWS::CloudWatch::Alarm" => self.create_cloudwatch_alarm(resource),
1189 "AWS::CloudWatch::Dashboard" => self.create_cloudwatch_dashboard(resource),
1190 "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1191 self.create_elbv2_load_balancer(resource)
1192 }
1193 "AWS::ElasticLoadBalancingV2::TargetGroup" => self.create_elbv2_target_group(resource),
1194 "AWS::ElasticLoadBalancingV2::Listener" => self.create_elbv2_listener(resource),
1195 "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1196 self.create_elbv2_listener_rule(resource)
1197 }
1198 "AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
1199 self.create_elbv2_listener_certificate(resource)
1200 }
1201 "AWS::ElasticLoadBalancingV2::TrustStore" => self.create_elbv2_trust_store(resource),
1202 "AWS::Organizations::Organization" => self.create_organization(resource),
1203 "AWS::Organizations::OrganizationalUnit" => self.create_organization_unit(resource),
1204 "AWS::Organizations::Account" => self.create_organization_account(resource),
1205 "AWS::Organizations::Policy" => self.create_organization_policy(resource),
1206 "AWS::Organizations::ResourcePolicy" => {
1207 self.create_organization_resource_policy(resource)
1208 }
1209 "AWS::Cognito::UserPool" => self.create_cognito_user_pool(resource),
1210 "AWS::Cognito::UserPoolClient" => self.create_cognito_user_pool_client(resource),
1211 "AWS::Cognito::UserPoolDomain" => self.create_cognito_user_pool_domain(resource),
1212 "AWS::Cognito::IdentityPool" => self.create_cognito_identity_pool(resource),
1213 "AWS::Cognito::IdentityPoolRoleAttachment" => {
1214 self.create_cognito_identity_pool_role_attachment(resource)
1215 }
1216 "AWS::RDS::DBSubnetGroup" => self.create_rds_subnet_group(resource),
1217 "AWS::RDS::DBParameterGroup" => self.create_rds_parameter_group(resource),
1218 "AWS::RDS::DBClusterParameterGroup" => {
1219 self.create_rds_cluster_parameter_group(resource)
1220 }
1221 "AWS::RDS::OptionGroup" => self.create_rds_option_group(resource),
1222 "AWS::RDS::EventSubscription" => self.create_rds_event_subscription(resource),
1223 "AWS::RDS::DBSecurityGroup" => self.create_rds_security_group(resource),
1224 "AWS::RDS::DBProxy" => self.create_rds_db_proxy(resource),
1225 "AWS::RDS::DBInstance" => self.create_rds_db_instance(resource),
1226 "AWS::RDS::DBCluster" => self.create_rds_db_cluster(resource),
1227 "AWS::AutoScaling::LaunchConfiguration" => {
1228 self.create_autoscaling_launch_configuration(resource)
1229 }
1230 "AWS::AutoScaling::AutoScalingGroup" => self.create_autoscaling_group(resource),
1231 "AWS::Batch::ComputeEnvironment" => self.create_batch_compute_environment(resource),
1232 "AWS::Batch::JobQueue" => self.create_batch_job_queue(resource),
1233 "AWS::Batch::JobDefinition" => self.create_batch_job_definition(resource),
1234 "AWS::Batch::SchedulingPolicy" => self.create_batch_scheduling_policy(resource),
1235 "AWS::Pipes::Pipe" => self.create_pipes_pipe(resource),
1236 "AWS::CodeArtifact::Domain" => self.create_codeartifact_domain(resource),
1237 "AWS::CodeArtifact::Repository" => self.create_codeartifact_repository(resource),
1238 "AWS::AmazonMQ::Broker" => self.create_mq_broker(resource),
1239 "AWS::AmazonMQ::Configuration" => self.create_mq_configuration(resource),
1240 "AWS::AmazonMQ::ConfigurationAssociation" => {
1241 self.create_mq_configuration_association(resource)
1242 }
1243 "AWS::MSK::Cluster" => self.create_msk_cluster(resource),
1244 "AWS::MSK::ServerlessCluster" => self.create_msk_serverless_cluster(resource),
1245 "AWS::MSK::Configuration" => self.create_msk_configuration(resource),
1246 "AWS::MSK::ClusterPolicy" => self.create_msk_cluster_policy(resource),
1247 "AWS::MSK::BatchScramSecret" => self.create_msk_batch_scram_secret(resource),
1248 "AWS::MSK::VpcConnection" => self.create_msk_vpc_connection(resource),
1249 "AWS::MSK::Replicator" => self.create_msk_replicator(resource),
1250 "AWS::KinesisAnalyticsV2::Application" => self.create_ka2_application(resource),
1251 "AWS::KinesisAnalyticsV2::ApplicationOutput" => {
1252 self.create_ka2_application_output(resource)
1253 }
1254 "AWS::KinesisAnalyticsV2::ApplicationReferenceDataSource" => {
1255 self.create_ka2_reference_data_source(resource)
1256 }
1257 "AWS::KinesisAnalyticsV2::ApplicationCloudWatchLoggingOption" => {
1258 self.create_ka2_cloudwatch_logging_option(resource)
1259 }
1260 "AWS::CodeCommit::Repository" => self.create_codecommit_repository(resource),
1261 "AWS::EFS::FileSystem" => self.create_efs_file_system(resource),
1262 "AWS::EFS::MountTarget" => self.create_efs_mount_target(resource),
1263 "AWS::EFS::AccessPoint" => self.create_efs_access_point(resource),
1264 "AWS::ElasticBeanstalk::Application" => self.create_eb_application(resource),
1265 "AWS::ElasticBeanstalk::ApplicationVersion" => {
1266 self.create_eb_application_version(resource)
1267 }
1268 "AWS::ElasticBeanstalk::Environment" => self.create_eb_environment(resource),
1269 "AWS::ElasticBeanstalk::ConfigurationTemplate" => {
1270 self.create_eb_configuration_template(resource)
1271 }
1272 "AWS::EC2::VPC" => self.create_ec2_vpc(resource),
1273 "AWS::EC2::Instance" => self.create_ec2_instance(resource),
1274 "AWS::EC2::Subnet" => self.create_ec2_subnet(resource),
1275 "AWS::EC2::SecurityGroup" => self.create_ec2_security_group(resource),
1276 "AWS::EC2::InternetGateway" => self.create_ec2_internet_gateway(resource),
1277 "AWS::EC2::RouteTable" => self.create_ec2_route_table(resource),
1278 "AWS::ECS::Cluster" => self.create_ecs_cluster(resource),
1279 "AWS::ECS::TaskDefinition" => self.create_ecs_task_definition(resource),
1280 "AWS::ECS::Service" => self.create_ecs_service(resource),
1281 "AWS::ECS::CapacityProvider" => self.create_ecs_capacity_provider(resource),
1282 "AWS::CertificateManager::Certificate" => self.create_acm_certificate(resource),
1283 "AWS::CertificateManager::Account" => self.create_acm_account(resource),
1284 "AWS::ACMPCA::CertificateAuthority" => {
1285 self.create_acmpca_certificate_authority(resource)
1286 }
1287 "AWS::ACMPCA::Certificate" => self.create_acmpca_certificate(resource),
1288 "AWS::ACMPCA::CertificateAuthorityActivation" => {
1289 self.create_acmpca_certificate_authority_activation(resource)
1290 }
1291 "AWS::ACMPCA::Permission" => self.create_acmpca_permission(resource),
1292 "AWS::Route53Resolver::ResolverEndpoint" => {
1293 self.create_r53r_resolver_endpoint(resource)
1294 }
1295 "AWS::Route53Resolver::ResolverRule" => self.create_r53r_resolver_rule(resource),
1296 "AWS::Route53Resolver::ResolverRuleAssociation" => {
1297 self.create_r53r_rule_association(resource)
1298 }
1299 "AWS::Route53Resolver::ResolverQueryLoggingConfig" => {
1300 self.create_r53r_query_log_config(resource)
1301 }
1302 "AWS::Route53Resolver::ResolverQueryLoggingConfigAssociation" => {
1303 self.create_r53r_query_log_association(resource)
1304 }
1305 "AWS::Route53Resolver::FirewallDomainList" => {
1306 self.create_r53r_firewall_domain_list(resource)
1307 }
1308 "AWS::Route53Resolver::FirewallRuleGroup" => {
1309 self.create_r53r_firewall_rule_group(resource)
1310 }
1311 "AWS::Route53Resolver::FirewallRuleGroupAssociation" => {
1312 self.create_r53r_firewall_rule_group_association(resource)
1313 }
1314 "AWS::Route53Resolver::FirewallConfig" => self.create_r53r_firewall_config(resource),
1315 "AWS::Route53Resolver::ResolverConfig" => self.create_r53r_resolver_config(resource),
1316 "AWS::Route53Resolver::ResolverDNSSECConfig" => {
1317 self.create_r53r_dnssec_config(resource)
1318 }
1319 "AWS::Config::ConfigurationRecorder"
1320 | "AWS::Config::DeliveryChannel"
1321 | "AWS::Config::ConfigRule"
1322 | "AWS::Config::ConfigurationAggregator"
1323 | "AWS::Config::AggregationAuthorization"
1324 | "AWS::Config::ConformancePack"
1325 | "AWS::Config::OrganizationConfigRule" => self.create_config_resource(resource),
1326 "AWS::ElastiCache::ParameterGroup" => self.create_ec_parameter_group(resource),
1327 "AWS::ElastiCache::SubnetGroup" => self.create_ec_subnet_group(resource),
1328 "AWS::ElastiCache::SecurityGroup" => self.create_ec_security_group(resource),
1329 "AWS::ElastiCache::User" => self.create_ec_user(resource),
1330 "AWS::ElastiCache::UserGroup" => self.create_ec_user_group(resource),
1331 "AWS::ElastiCache::CacheCluster" => self.create_ec_cache_cluster(resource),
1332 "AWS::ElastiCache::ReplicationGroup" => self.create_ec_replication_group(resource),
1333 "AWS::Route53::HostedZone" => self.create_route53_hosted_zone(resource),
1334 "AWS::Route53::RecordSet" => self.create_route53_record_set(resource),
1335 "AWS::Route53::HealthCheck" => self.create_route53_health_check(resource),
1336 "AWS::Route53::DNSSEC" => self.create_route53_dnssec(resource),
1337 "AWS::Route53::KeySigningKey" => self.create_route53_key_signing_key(resource),
1338 "AWS::CloudFront::CloudFrontOriginAccessIdentity" => {
1339 self.create_cf_origin_access_identity(resource)
1340 }
1341 "AWS::CloudFront::Distribution" => self.create_cf_distribution(resource),
1342 "AWS::CloudFront::OriginAccessControl" => {
1343 self.create_cf_origin_access_control(resource)
1344 }
1345 "AWS::CloudFront::PublicKey" => self.create_cf_public_key(resource),
1346 "AWS::CloudFront::KeyGroup" => self.create_cf_key_group(resource),
1347 "AWS::CloudFront::Function" => self.create_cf_function(resource),
1348 "AWS::CloudFront::CachePolicy" => self.create_cf_cache_policy(resource),
1349 "AWS::CloudFront::OriginRequestPolicy" => {
1350 self.create_cf_origin_request_policy(resource)
1351 }
1352 "AWS::CloudFront::ResponseHeadersPolicy" => {
1353 self.create_cf_response_headers_policy(resource)
1354 }
1355 "AWS::StepFunctions::StateMachine" => self.create_sfn_state_machine(resource),
1356 "AWS::StepFunctions::Activity" => self.create_sfn_activity(resource),
1357 "AWS::StepFunctions::StateMachineVersion" => self.create_sfn_version(resource),
1358 "AWS::StepFunctions::StateMachineAlias" => self.create_sfn_alias(resource),
1359 "AWS::WAFv2::WebACL" => self.create_wafv2_web_acl(resource),
1360 "AWS::WAFv2::IPSet" => self.create_wafv2_ip_set(resource),
1361 "AWS::WAFv2::RegexPatternSet" => self.create_wafv2_regex_pattern_set(resource),
1362 "AWS::WAFv2::RuleGroup" => self.create_wafv2_rule_group(resource),
1363 "AWS::WAFv2::LoggingConfiguration" => self.create_wafv2_logging_configuration(resource),
1364 "AWS::WAFv2::WebACLAssociation" => self.create_wafv2_web_acl_association(resource),
1365 "AWS::ApiGateway::RestApi" => self.create_apigw_rest_api(resource),
1366 "AWS::ApiGateway::Resource" => self.create_apigw_resource(resource),
1367 "AWS::ApiGateway::Method" => self.create_apigw_method(resource),
1368 "AWS::ApiGateway::Deployment" => self.create_apigw_deployment(resource),
1369 "AWS::ApiGateway::Stage" => self.create_apigw_stage(resource),
1370 "AWS::ApiGateway::Authorizer" => self.create_apigw_authorizer(resource),
1371 "AWS::ApiGateway::RequestValidator" => self.create_apigw_request_validator(resource),
1372 "AWS::ApiGateway::Model" => self.create_apigw_model(resource),
1373 "AWS::ApiGateway::GatewayResponse" => self.create_apigw_gateway_response(resource),
1374 "AWS::ApiGateway::UsagePlan" => self.create_apigw_usage_plan(resource),
1375 "AWS::ApiGateway::ApiKey" => self.create_apigw_api_key(resource),
1376 "AWS::ApiGateway::UsagePlanKey" => self.create_apigw_usage_plan_key(resource),
1377 "AWS::ApiGateway::DomainName" => self.create_apigw_domain_name(resource),
1378 "AWS::ApiGateway::BasePathMapping" => self.create_apigw_base_path_mapping(resource),
1379 "AWS::ApiGatewayV2::Api" => self.create_apigwv2_api(resource),
1380 "AWS::ApiGatewayV2::Route" => self.create_apigwv2_route(resource),
1381 "AWS::ApiGatewayV2::Integration" => self.create_apigwv2_integration(resource),
1382 "AWS::ApiGatewayV2::IntegrationResponse" => {
1383 self.create_apigwv2_integration_response(resource)
1384 }
1385 "AWS::ApiGatewayV2::RouteResponse" => self.create_apigwv2_route_response(resource),
1386 "AWS::ApiGatewayV2::Stage" => self.create_apigwv2_stage(resource),
1387 "AWS::ApiGatewayV2::Deployment" => self.create_apigwv2_deployment(resource),
1388 "AWS::ApiGatewayV2::Authorizer" => self.create_apigwv2_authorizer(resource),
1389 "AWS::ApiGatewayV2::DomainName" => self.create_apigwv2_domain_name(resource),
1390 "AWS::ApiGatewayV2::ApiMapping" => self.create_apigwv2_api_mapping(resource),
1391 "AWS::ApiGatewayV2::VpcLink" => self.create_apigwv2_vpc_link(resource),
1392 "AWS::ApiGatewayV2::Model" => self.create_apigwv2_model(resource),
1393 "AWS::SES::ConfigurationSet" => self.create_ses_configuration_set(resource),
1394 "AWS::SES::ConfigurationSetEventDestination" => {
1395 self.create_ses_event_destination(resource)
1396 }
1397 "AWS::SES::EmailIdentity" => self.create_ses_email_identity(resource),
1398 "AWS::SES::Template" => self.create_ses_template(resource),
1399 "AWS::SES::ContactList" => self.create_ses_contact_list(resource),
1400 "AWS::SES::DedicatedIpPool" => self.create_ses_dedicated_ip_pool(resource),
1401 "AWS::SES::ReceiptRule" => self.create_ses_receipt_rule(resource),
1402 "AWS::SES::ReceiptRuleSet" => self.create_ses_receipt_rule_set(resource),
1403 "AWS::SES::ReceiptFilter" => self.create_ses_receipt_filter(resource),
1404 "AWS::SES::VdmAttributes" => self.create_ses_vdm_attributes(resource),
1405 "AWS::SecretsManager::RotationSchedule" => {
1406 self.create_secrets_manager_rotation_schedule(resource)
1407 }
1408 "AWS::SecretsManager::ResourcePolicy" => {
1409 self.create_secrets_manager_resource_policy(resource)
1410 }
1411 "AWS::SecretsManager::SecretTargetAttachment" => {
1412 self.create_secrets_manager_target_attachment(resource)
1413 }
1414 "AWS::ApplicationAutoScaling::ScalableTarget" => {
1415 self.create_application_autoscaling_scalable_target(resource)
1416 }
1417 "AWS::ApplicationAutoScaling::ScalingPolicy" => {
1418 self.create_application_autoscaling_scaling_policy(resource)
1419 }
1420 "AWS::Athena::DataCatalog" => self.create_athena_data_catalog(resource),
1421 "AWS::Athena::NamedQuery" => self.create_athena_named_query(resource),
1422 "AWS::Athena::WorkGroup" => self.create_athena_work_group(resource),
1423 "AWS::Athena::PreparedStatement" => self.create_athena_prepared_statement(resource),
1424 "AWS::KinesisFirehose::DeliveryStream" => {
1425 self.create_firehose_delivery_stream(resource)
1426 }
1427 "AWS::Glue::Database" => self.create_glue_database(resource),
1428 "AWS::EKS::Cluster" => self.create_eks_cluster(resource),
1429 "AWS::EKS::Nodegroup" => self.create_eks_nodegroup(resource),
1430 "AWS::EKS::FargateProfile" => self.create_eks_fargate_profile(resource),
1431 "AWS::EKS::Addon" => self.create_eks_addon(resource),
1432 "AWS::EKS::AccessEntry" => self.create_eks_access_entry(resource),
1433 "AWS::EKS::IdentityProviderConfig" => {
1434 self.create_eks_identity_provider_config(resource)
1435 }
1436 "AWS::EKS::PodIdentityAssociation" => {
1437 self.create_eks_pod_identity_association(resource)
1438 }
1439 "AWS::ServiceDiscovery::HttpNamespace" => self.create_sd_http_namespace(resource),
1440 "AWS::ServiceDiscovery::PublicDnsNamespace" => {
1441 self.create_sd_public_dns_namespace(resource)
1442 }
1443 "AWS::ServiceDiscovery::PrivateDnsNamespace" => {
1444 self.create_sd_private_dns_namespace(resource)
1445 }
1446 "AWS::ServiceDiscovery::Service" => self.create_sd_service(resource),
1447 "AWS::ServiceDiscovery::Instance" => self.create_sd_instance(resource),
1448 "AWS::CloudFormation::Stack" => self.create_cloudformation_stack(resource),
1449 "AWS::Glue::Table" => self.create_glue_table(resource),
1450 "AWS::Glue::Partition" => self.create_glue_partition(resource),
1451 t if t.starts_with("Custom::") || t == "AWS::CloudFormation::CustomResource" => self
1452 .create_custom_resource(resource)
1453 .map(ProvisionResult::new),
1454 other if self.strict_unknown_types => {
1455 return Err(format!(
1460 "Resource type '{other}' is not supported by Cloud Control API on fakecloud."
1461 ));
1462 }
1463 other => {
1464 tracing::warn!(
1475 resource_type = %other,
1476 logical_id = %resource.logical_id,
1477 "CloudFormation: no provisioner for resource type; recording it as provisioned with no backing state"
1478 );
1479 Ok(ProvisionResult::new(resource.logical_id.clone()))
1480 }
1481 };
1482
1483 let is_custom = resource.resource_type.starts_with("Custom::")
1484 || resource.resource_type == "AWS::CloudFormation::CustomResource";
1485 let service_token = if is_custom {
1486 resource
1487 .properties
1488 .get("ServiceToken")
1489 .and_then(|v| v.as_str())
1490 .map(|s| s.to_string())
1491 } else {
1492 None
1493 };
1494
1495 result.map(|res| StackResource {
1496 logical_id: resource.logical_id.clone(),
1497 physical_id: res.physical_id,
1498 resource_type: resource.resource_type.clone(),
1499 status: "CREATE_COMPLETE".to_string(),
1500 service_token,
1501 attributes: res.attributes,
1502 })
1503 }
1504
1505 pub fn update_resource(
1512 &self,
1513 existing: &StackResource,
1514 new_def: &ResourceDefinition,
1515 ) -> Result<Option<StackResource>, String> {
1516 let result = match new_def.resource_type.as_str() {
1517 "AWS::Lambda::Function" => Some(self.update_lambda_function(existing, new_def)?),
1518 "AWS::Lambda::Permission" => Some(self.update_lambda_permission(existing, new_def)?),
1519 "AWS::Lambda::EventSourceMapping" => {
1520 Some(self.update_lambda_event_source_mapping(existing, new_def)?)
1521 }
1522 "AWS::Lambda::LayerVersion" => {
1523 Some(self.update_lambda_layer_version(existing, new_def)?)
1524 }
1525 "AWS::Lambda::Url" => Some(self.update_lambda_url(existing, new_def)?),
1526 "AWS::Lambda::Alias" => Some(self.update_lambda_alias(existing, new_def)?),
1527 "AWS::Lambda::Version" => Some(self.update_lambda_version(existing, new_def)?),
1528 "AWS::IAM::Role" => Some(self.update_iam_role(existing, new_def)?),
1529 "AWS::IAM::Policy" => Some(self.update_iam_policy(existing, new_def)?),
1530 "AWS::IAM::ManagedPolicy" => Some(self.update_iam_policy(existing, new_def)?),
1531 "AWS::ApiGateway::RestApi" => Some(self.update_apigw_rest_api(existing, new_def)?),
1532 "AWS::ApiGateway::Resource" => Some(self.update_apigw_resource(existing, new_def)?),
1533 "AWS::ApiGateway::Method" => Some(self.update_apigw_method(existing, new_def)?),
1534 "AWS::ApiGateway::Deployment" => Some(self.update_apigw_deployment(existing, new_def)?),
1535 "AWS::ApiGateway::Stage" => Some(self.update_apigw_stage(existing, new_def)?),
1536 "AWS::ApiGateway::Authorizer" => Some(self.update_apigw_authorizer(existing, new_def)?),
1537 "AWS::ApiGateway::RequestValidator" => {
1538 Some(self.update_apigw_request_validator(existing, new_def)?)
1539 }
1540 "AWS::ApiGateway::Model" => Some(self.update_apigw_model(existing, new_def)?),
1541 "AWS::ApiGateway::GatewayResponse" => {
1542 Some(self.update_apigw_gateway_response(existing, new_def)?)
1543 }
1544 "AWS::ApiGateway::UsagePlan" => Some(self.update_apigw_usage_plan(existing, new_def)?),
1545 "AWS::ApiGateway::ApiKey" => Some(self.update_apigw_api_key(existing, new_def)?),
1546 "AWS::ApiGateway::UsagePlanKey" => {
1547 Some(self.update_apigw_usage_plan_key(existing, new_def)?)
1548 }
1549 "AWS::ApiGateway::DomainName" => {
1550 Some(self.update_apigw_domain_name(existing, new_def)?)
1551 }
1552 "AWS::ApiGateway::BasePathMapping" => {
1553 Some(self.update_apigw_base_path_mapping(existing, new_def)?)
1554 }
1555 "AWS::ApiGatewayV2::Api" => Some(self.update_apigwv2_api(existing, new_def)?),
1556 "AWS::ApiGatewayV2::Route" => Some(self.update_apigwv2_route(existing, new_def)?),
1557 "AWS::ApiGatewayV2::Integration" => {
1558 Some(self.update_apigwv2_integration(existing, new_def)?)
1559 }
1560 "AWS::ApiGatewayV2::IntegrationResponse" => {
1561 Some(self.update_apigwv2_integration_response(existing, new_def)?)
1562 }
1563 "AWS::ApiGatewayV2::RouteResponse" => {
1564 Some(self.update_apigwv2_route_response(existing, new_def)?)
1565 }
1566 "AWS::ApiGatewayV2::Stage" => Some(self.update_apigwv2_stage(existing, new_def)?),
1567 "AWS::ApiGatewayV2::Deployment" => {
1568 Some(self.update_apigwv2_deployment(existing, new_def)?)
1569 }
1570 "AWS::ApiGatewayV2::Authorizer" => {
1571 Some(self.update_apigwv2_authorizer(existing, new_def)?)
1572 }
1573 "AWS::ApiGatewayV2::DomainName" => {
1574 Some(self.update_apigwv2_domain_name(existing, new_def)?)
1575 }
1576 "AWS::ApiGatewayV2::ApiMapping" => {
1577 Some(self.update_apigwv2_api_mapping(existing, new_def)?)
1578 }
1579 "AWS::ApiGatewayV2::VpcLink" => Some(self.update_apigwv2_vpc_link(existing, new_def)?),
1580 "AWS::ApiGatewayV2::Model" => Some(self.update_apigwv2_model(existing, new_def)?),
1581 "AWS::ECS::Cluster" => Some(self.update_ecs_cluster(existing, new_def)?),
1582 "AWS::ECS::Service" => Some(self.update_ecs_service(existing, new_def)?),
1583 "AWS::ECS::TaskDefinition" => Some(self.update_ecs_task_definition(existing, new_def)?),
1584 "AWS::ECS::CapacityProvider" => {
1585 Some(self.update_ecs_capacity_provider(existing, new_def)?)
1586 }
1587 "AWS::ECR::Repository" => Some(self.update_ecr_repository(existing, new_def)?),
1588 "AWS::ECR::RepositoryPolicy" => {
1589 Some(self.update_ecr_repository_policy(existing, new_def)?)
1590 }
1591 "AWS::ECR::LifecyclePolicy" => {
1592 Some(self.update_ecr_lifecycle_policy(existing, new_def)?)
1593 }
1594 "AWS::ECR::RegistryPolicy" => Some(self.update_ecr_registry_policy(existing, new_def)?),
1595 "AWS::ECR::ReplicationConfiguration" => {
1596 Some(self.update_ecr_replication_configuration(existing, new_def)?)
1597 }
1598 "AWS::ECR::RegistryScanningConfiguration" => {
1599 Some(self.update_ecr_registry_scanning_configuration(existing, new_def)?)
1600 }
1601 "AWS::ECR::PullThroughCacheRule" => {
1602 Some(self.update_ecr_pull_through_cache_rule(existing, new_def)?)
1603 }
1604 "AWS::KMS::Key" => Some(self.update_kms_key(existing, new_def)?),
1605 "AWS::KMS::ReplicaKey" => Some(self.update_kms_replica_key(existing, new_def)?),
1606 "AWS::KMS::Alias" => Some(self.update_kms_alias(existing, new_def)?),
1607 "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1608 Some(self.update_elbv2_load_balancer(existing, new_def)?)
1609 }
1610 "AWS::ElasticLoadBalancingV2::TargetGroup" => {
1611 Some(self.update_elbv2_target_group(existing, new_def)?)
1612 }
1613 "AWS::ElasticLoadBalancingV2::Listener" => {
1614 Some(self.update_elbv2_listener(existing, new_def)?)
1615 }
1616 "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1617 Some(self.update_elbv2_listener_rule(existing, new_def)?)
1618 }
1619 "AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
1620 Some(self.update_elbv2_listener_certificate(existing, new_def)?)
1621 }
1622 "AWS::ElasticLoadBalancingV2::TrustStore" => {
1623 Some(self.update_elbv2_trust_store(existing, new_def)?)
1624 }
1625 "AWS::CloudWatch::Alarm" => Some(self.update_cloudwatch_alarm(existing, new_def)?),
1626 "AWS::CloudWatch::Dashboard" => {
1627 Some(self.update_cloudwatch_dashboard(existing, new_def)?)
1628 }
1629 "AWS::StepFunctions::StateMachine" => {
1630 Some(self.update_sfn_state_machine(existing, new_def)?)
1631 }
1632 "AWS::SQS::Queue" => Some(self.update_sqs_queue(existing, new_def)?),
1633 "AWS::SQS::QueuePolicy" => Some(self.update_sqs_queue_policy(existing, new_def)?),
1634 "AWS::SNS::Topic" => Some(self.update_sns_topic(existing, new_def)?),
1635 "AWS::SNS::TopicPolicy" => Some(self.update_sns_topic_policy(existing, new_def)?),
1636 "AWS::S3::Bucket" => Some(self.update_s3_bucket(existing, new_def)?),
1637 "AWS::S3::BucketPolicy" => Some(self.update_s3_bucket_policy(existing, new_def)?),
1638 "AWS::Pipes::Pipe" => Some(self.update_pipes_pipe(existing, new_def)?),
1639 "AWS::CodeArtifact::Domain" => {
1640 Some(self.update_codeartifact_domain(existing, new_def)?)
1641 }
1642 "AWS::CodeArtifact::Repository" => {
1643 Some(self.update_codeartifact_repository(existing, new_def)?)
1644 }
1645 "AWS::AmazonMQ::Broker" => Some(self.update_mq_broker(existing, new_def)?),
1646 "AWS::AmazonMQ::Configuration" => {
1647 Some(self.update_mq_configuration(existing, new_def)?)
1648 }
1649 "AWS::AmazonMQ::ConfigurationAssociation" => {
1650 Some(self.create_mq_configuration_association(new_def)?)
1651 }
1652 "AWS::MSK::Cluster" => Some(self.update_msk_cluster(existing, new_def)?),
1653 "AWS::MSK::ServerlessCluster" => Some(self.update_msk_cluster(existing, new_def)?),
1654 "AWS::MSK::Configuration" => Some(self.update_msk_configuration(existing, new_def)?),
1655 "AWS::MSK::ClusterPolicy" => Some(self.update_msk_cluster_policy(existing, new_def)?),
1656 "AWS::MSK::BatchScramSecret" => {
1657 Some(self.update_msk_batch_scram_secret(existing, new_def)?)
1658 }
1659 "AWS::MSK::VpcConnection" => Some(self.update_msk_vpc_connection(existing, new_def)?),
1660 "AWS::MSK::Replicator" => Some(self.update_msk_replicator(existing, new_def)?),
1661 "AWS::KinesisAnalyticsV2::Application" => {
1662 Some(self.update_ka2_application(existing, new_def)?)
1663 }
1664 "AWS::KinesisAnalyticsV2::ApplicationOutput" => {
1665 Some(self.update_ka2_application_output(existing, new_def)?)
1666 }
1667 "AWS::KinesisAnalyticsV2::ApplicationReferenceDataSource" => {
1668 Some(self.update_ka2_reference_data_source(existing, new_def)?)
1669 }
1670 "AWS::KinesisAnalyticsV2::ApplicationCloudWatchLoggingOption" => {
1671 Some(self.update_ka2_cloudwatch_logging_option(existing, new_def)?)
1672 }
1673 "AWS::CodeCommit::Repository" => {
1674 Some(self.update_codecommit_repository(existing, new_def)?)
1675 }
1676 "AWS::EFS::FileSystem" => Some(self.update_efs_file_system(existing, new_def)?),
1677 "AWS::EFS::MountTarget" => Some(self.update_efs_mount_target(existing, new_def)?),
1678 "AWS::EFS::AccessPoint" => Some(self.update_efs_access_point(existing, new_def)?),
1679 "AWS::ElasticBeanstalk::Application" => {
1680 Some(self.update_eb_application(existing, new_def)?)
1681 }
1682 "AWS::ElasticBeanstalk::ApplicationVersion" => {
1683 Some(self.update_eb_application_version(existing, new_def)?)
1684 }
1685 "AWS::ElasticBeanstalk::Environment" => {
1686 Some(self.update_eb_environment(existing, new_def)?)
1687 }
1688 "AWS::ElasticBeanstalk::ConfigurationTemplate" => {
1689 Some(self.update_eb_configuration_template(existing, new_def)?)
1690 }
1691 _ => None,
1692 };
1693
1694 Ok(result.map(|res| StackResource {
1695 logical_id: existing.logical_id.clone(),
1696 physical_id: res.physical_id,
1697 resource_type: existing.resource_type.clone(),
1698 status: "UPDATE_COMPLETE".to_string(),
1699 service_token: existing.service_token.clone(),
1700 attributes: res.attributes,
1701 }))
1702 }
1703
1704 pub fn get_att(&self, resource: &StackResource, attribute: &str) -> Option<String> {
1715 const MQ_BROKER_LIVE_ATTRS: &[&str] = &[
1722 "IpAddresses",
1723 "OpenWireEndpoints",
1724 "AmqpEndpoints",
1725 "StompEndpoints",
1726 "MqttEndpoints",
1727 "WssEndpoints",
1728 ];
1729 if resource.resource_type == "AWS::AmazonMQ::Broker"
1730 && MQ_BROKER_LIVE_ATTRS.contains(&attribute)
1731 {
1732 if let Some(v) = self.get_att_mq_broker(&resource.physical_id, attribute) {
1733 return Some(v);
1734 }
1735 }
1736 if let Some(v) = resource.attributes.get(attribute) {
1739 return Some(v.clone());
1740 }
1741 match resource.resource_type.as_str() {
1744 "AWS::S3::Bucket" => self.get_att_s3_bucket(&resource.physical_id, attribute),
1745 "AWS::Lambda::Function" => {
1746 self.get_att_lambda_function(&resource.physical_id, attribute)
1747 }
1748 "AWS::IAM::Role" => self.get_att_iam_role(&resource.physical_id, attribute),
1749 "AWS::SQS::Queue" => self.get_att_sqs_queue(&resource.physical_id, attribute),
1750 "AWS::SNS::Topic" => self.get_att_sns_topic(&resource.physical_id, attribute),
1751 "AWS::DynamoDB::Table" => self.get_att_dynamodb_table(&resource.physical_id, attribute),
1752 "AWS::KMS::Key" => self.get_att_kms_key(&resource.physical_id, attribute),
1753 "AWS::SecretsManager::Secret" => {
1754 self.get_att_secrets_manager_secret(&resource.physical_id, attribute)
1755 }
1756 "AWS::CloudFront::Distribution" => {
1757 self.get_att_cf_distribution(&resource.physical_id, attribute)
1758 }
1759 "AWS::ECS::Cluster" => self.get_att_ecs_cluster(&resource.physical_id, attribute),
1760 "AWS::ECS::Service" => self.get_att_ecs_service(&resource.physical_id, attribute),
1761 "AWS::EC2::VPC"
1762 | "AWS::EC2::Subnet"
1763 | "AWS::EC2::SecurityGroup"
1764 | "AWS::EC2::InternetGateway"
1765 | "AWS::EC2::Instance"
1766 | "AWS::EC2::RouteTable" => self.get_att_ec2(resource, attribute),
1767 "AWS::ECS::CapacityProvider" => {
1768 self.get_att_ecs_capacity_provider(&resource.physical_id, attribute)
1769 }
1770 "AWS::ECR::Repository" => self.get_att_ecr_repository(&resource.physical_id, attribute),
1771 "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1772 self.get_att_elbv2_load_balancer(&resource.physical_id, attribute)
1773 }
1774 "AWS::ElasticLoadBalancingV2::TargetGroup" => {
1775 self.get_att_elbv2_target_group(&resource.physical_id, attribute)
1776 }
1777 "AWS::ElasticLoadBalancingV2::Listener" => {
1778 self.get_att_elbv2_listener(&resource.physical_id, attribute)
1779 }
1780 "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1781 self.get_att_elbv2_listener_rule(&resource.physical_id, attribute)
1782 }
1783 "AWS::ElasticLoadBalancingV2::TrustStore" => {
1784 self.get_att_elbv2_trust_store(&resource.physical_id, attribute)
1785 }
1786 "AWS::WAFv2::WebACL" => self.get_att_wafv2_web_acl(&resource.physical_id, attribute),
1787 "AWS::WAFv2::IPSet" => self.get_att_wafv2_ip_set(&resource.physical_id, attribute),
1788 "AWS::WAFv2::RegexPatternSet" => {
1789 self.get_att_wafv2_regex_pattern_set(&resource.physical_id, attribute)
1790 }
1791 "AWS::WAFv2::RuleGroup" => {
1792 self.get_att_wafv2_rule_group(&resource.physical_id, attribute)
1793 }
1794 "AWS::SES::ConfigurationSet" => {
1795 self.get_att_ses_configuration_set(&resource.physical_id, attribute)
1796 }
1797 "AWS::SES::EmailIdentity" => {
1798 self.get_att_ses_email_identity(&resource.physical_id, attribute)
1799 }
1800 "AWS::SES::Template" => self.get_att_ses_template(&resource.physical_id, attribute),
1801 "AWS::SES::ContactList" => {
1802 self.get_att_ses_contact_list(&resource.physical_id, attribute)
1803 }
1804 "AWS::SES::DedicatedIpPool" => {
1805 self.get_att_ses_dedicated_ip_pool(&resource.physical_id, attribute)
1806 }
1807 "AWS::SES::ReceiptRuleSet" => {
1808 self.get_att_ses_receipt_rule_set(&resource.physical_id, attribute)
1809 }
1810 "AWS::Athena::DataCatalog" => {
1811 self.get_att_athena_data_catalog(&resource.physical_id, attribute)
1812 }
1813 "AWS::Athena::NamedQuery" => {
1814 self.get_att_athena_named_query(&resource.physical_id, attribute)
1815 }
1816 "AWS::Athena::WorkGroup" => {
1817 self.get_att_athena_work_group(&resource.physical_id, attribute)
1818 }
1819 "AWS::Athena::PreparedStatement" => {
1820 self.get_att_athena_prepared_statement(&resource.physical_id, attribute)
1821 }
1822 "AWS::CloudFormation::Stack" => {
1823 self.get_att_cloudformation_stack(&resource.physical_id, attribute)
1824 }
1825 "AWS::Pipes::Pipe" => self.get_att_pipes_pipe(&resource.physical_id, attribute),
1826 "AWS::CodeArtifact::Domain" => {
1827 self.get_att_codeartifact_domain(&resource.physical_id, attribute)
1828 }
1829 "AWS::CodeArtifact::Repository" => {
1830 self.get_att_codeartifact_repository(&resource.physical_id, attribute)
1831 }
1832 "AWS::AmazonMQ::Broker" => self.get_att_mq_broker(&resource.physical_id, attribute),
1833 "AWS::AmazonMQ::Configuration" => {
1834 self.get_att_mq_configuration(&resource.physical_id, attribute)
1835 }
1836 "AWS::MSK::Cluster" | "AWS::MSK::ServerlessCluster" => {
1837 self.get_att_msk_cluster(&resource.physical_id, attribute)
1838 }
1839 "AWS::MSK::Configuration" => {
1840 self.get_att_msk_configuration(&resource.physical_id, attribute)
1841 }
1842 "AWS::MSK::ClusterPolicy" => {
1843 self.get_att_msk_cluster_policy(&resource.physical_id, attribute)
1844 }
1845 "AWS::MSK::VpcConnection" => {
1846 self.get_att_msk_vpc_connection(&resource.physical_id, attribute)
1847 }
1848 "AWS::MSK::Replicator" => self.get_att_msk_replicator(&resource.physical_id, attribute),
1849 "AWS::CodeCommit::Repository" => {
1850 self.get_att_codecommit_repository(&resource.physical_id, attribute)
1851 }
1852 "AWS::EFS::FileSystem" => {
1853 self.get_att_efs_file_system(&resource.physical_id, attribute)
1854 }
1855 "AWS::EFS::MountTarget" => {
1856 self.get_att_efs_mount_target(&resource.physical_id, attribute)
1857 }
1858 "AWS::EFS::AccessPoint" => {
1859 self.get_att_efs_access_point(&resource.physical_id, attribute)
1860 }
1861 "AWS::ElasticBeanstalk::Environment" => {
1862 self.get_att_eb_environment(&resource.physical_id, attribute)
1863 }
1864 "AWS::EKS::Cluster" => self.get_att_eks_cluster(&resource.physical_id, attribute),
1865 "AWS::EKS::Nodegroup" => self.get_att_eks_nodegroup(&resource.physical_id, attribute),
1866 "AWS::EKS::FargateProfile" => {
1867 self.get_att_eks_fargate_profile(&resource.physical_id, attribute)
1868 }
1869 "AWS::EKS::Addon" => self.get_att_eks_addon(&resource.physical_id, attribute),
1870 "AWS::EKS::AccessEntry" => {
1871 self.get_att_eks_access_entry(&resource.physical_id, attribute)
1872 }
1873 "AWS::EKS::IdentityProviderConfig" => {
1874 self.get_att_eks_identity_provider_config(&resource.physical_id, attribute)
1875 }
1876 "AWS::EKS::PodIdentityAssociation" => {
1877 self.get_att_eks_pod_identity_association(&resource.physical_id, attribute)
1878 }
1879 "AWS::ServiceDiscovery::HttpNamespace"
1880 | "AWS::ServiceDiscovery::PublicDnsNamespace"
1881 | "AWS::ServiceDiscovery::PrivateDnsNamespace" => {
1882 self.get_att_sd_namespace(&resource.physical_id, attribute)
1883 }
1884 "AWS::ServiceDiscovery::Service" => {
1885 self.get_att_sd_service(&resource.physical_id, attribute)
1886 }
1887 t if t.starts_with("AWS::Route53Resolver::") => {
1888 self.get_att_route53resolver(resource, attribute)
1889 }
1890 _ => None,
1891 }
1892 }
1893
1894 fn get_att_cf_distribution(&self, physical_id: &str, attribute: &str) -> Option<String> {
1895 let accounts = self.cloudfront_state.read();
1898 let state = accounts.get("000000000000")?;
1899 let dist = state.distributions.get(physical_id)?;
1900 match attribute {
1901 "DomainName" => Some(dist.domain_name.clone()),
1902 "Id" => Some(dist.id.clone()),
1903 _ => None,
1904 }
1905 }
1906
1907 pub fn delete_resource(&self, resource: &StackResource) -> Result<(), String> {
1909 match resource.resource_type.as_str() {
1910 "AWS::SQS::Queue" => self.delete_sqs_queue(&resource.physical_id),
1911 "AWS::SQS::QueuePolicy" => self.delete_sqs_queue_policy(&resource.physical_id),
1912 "AWS::SNS::Topic" => self.delete_sns_topic(&resource.physical_id),
1913 "AWS::SNS::TopicPolicy" => self.delete_sns_topic_policy(&resource.physical_id),
1914 "AWS::SNS::Subscription" => self.delete_sns_subscription(&resource.physical_id),
1915 "AWS::SSM::Parameter" => self.delete_ssm_parameter(&resource.physical_id),
1916 "AWS::IAM::Role" => self.delete_iam_role(&resource.physical_id),
1917 "AWS::IAM::Policy" => self.delete_iam_policy(&resource.physical_id),
1918 "AWS::IAM::User" => self.delete_iam_user(&resource.physical_id),
1919 "AWS::IAM::Group" => self.delete_iam_group(&resource.physical_id),
1920 "AWS::IAM::ManagedPolicy" => self.delete_iam_managed_policy(&resource.physical_id),
1921 "AWS::IAM::UserToGroupAddition" => {
1922 self.delete_iam_user_to_group_addition(&resource.physical_id)
1923 }
1924 "AWS::IAM::AccessKey" => self.delete_iam_access_key(&resource.physical_id),
1925 "AWS::IAM::InstanceProfile" => self.delete_iam_instance_profile(&resource.physical_id),
1926 "AWS::IAM::OIDCProvider" => self.delete_iam_oidc_provider(&resource.physical_id),
1927 "AWS::IAM::SAMLProvider" => self.delete_iam_saml_provider(&resource.physical_id),
1928 "AWS::IAM::ServiceLinkedRole" => {
1929 self.delete_iam_service_linked_role(&resource.physical_id)
1930 }
1931 "AWS::IAM::VirtualMFADevice" => {
1932 self.delete_iam_virtual_mfa_device(&resource.physical_id)
1933 }
1934 "AWS::S3::Bucket" => self.delete_s3_bucket(&resource.physical_id),
1935 "AWS::S3::BucketPolicy" => self.delete_s3_bucket_policy(&resource.physical_id),
1936 "AWS::Events::Rule" => self.delete_eventbridge_rule(&resource.physical_id),
1937 "AWS::Events::Connection" => self.delete_eventbridge_connection(&resource.physical_id),
1938 "AWS::Events::EventBus" => self.delete_eventbridge_event_bus(&resource.physical_id),
1939 "AWS::Events::EventBusPolicy" => {
1940 self.delete_eventbridge_event_bus_policy(&resource.physical_id)
1941 }
1942 "AWS::Events::Endpoint" => self.delete_eventbridge_endpoint(&resource.physical_id),
1943 "AWS::Events::ApiDestination" => {
1944 self.delete_eventbridge_api_destination(&resource.physical_id)
1945 }
1946 "AWS::Events::Archive" => self.delete_eventbridge_archive(&resource.physical_id),
1947 "AWS::DynamoDB::Table" => self.delete_dynamodb_table(&resource.physical_id),
1948 "AWS::Logs::LogGroup" => self.delete_log_group(&resource.physical_id),
1949 "AWS::Logs::LogStream" => self.delete_log_stream(&resource.physical_id),
1950 "AWS::Logs::MetricFilter" => self.delete_metric_filter(&resource.physical_id),
1951 "AWS::Logs::SubscriptionFilter" => {
1952 self.delete_subscription_filter(&resource.physical_id)
1953 }
1954 "AWS::Logs::Destination" => self.delete_logs_destination(&resource.physical_id),
1955 "AWS::Logs::ResourcePolicy" => self.delete_logs_resource_policy(&resource.physical_id),
1956 "AWS::Logs::QueryDefinition" => {
1957 self.delete_logs_query_definition(&resource.physical_id)
1958 }
1959 "AWS::Logs::Delivery" => self.delete_logs_delivery(&resource.physical_id),
1960 "AWS::Logs::DeliveryDestination" => {
1961 self.delete_logs_delivery_destination(&resource.physical_id)
1962 }
1963 "AWS::Logs::DeliverySource" => self.delete_logs_delivery_source(&resource.physical_id),
1964 "AWS::Lambda::Function" => self.delete_lambda_function(&resource.physical_id),
1965 "AWS::Lambda::Permission" => self.delete_lambda_permission(&resource.physical_id),
1966 "AWS::Lambda::EventSourceMapping" => {
1967 self.delete_lambda_event_source_mapping(&resource.physical_id)
1968 }
1969 "AWS::Lambda::LayerVersion" => self.delete_lambda_layer_version(&resource.physical_id),
1970 "AWS::Lambda::Url" => self.delete_lambda_url(&resource.physical_id),
1971 "AWS::Lambda::Alias" => self.delete_lambda_alias(&resource.physical_id),
1972 "AWS::Lambda::Version" => self.delete_lambda_version(&resource.physical_id),
1973 "AWS::SecretsManager::Secret" => {
1974 self.delete_secrets_manager_secret(&resource.physical_id)
1975 }
1976 "AWS::Kinesis::Stream" => self.delete_kinesis_stream(&resource.physical_id),
1977 "AWS::Kinesis::StreamConsumer" => {
1978 self.delete_kinesis_stream_consumer(&resource.physical_id)
1979 }
1980 "AWS::KMS::Key" => self.delete_kms_key(&resource.physical_id),
1981 "AWS::KMS::ReplicaKey" => self.delete_kms_replica_key(&resource.physical_id),
1982 "AWS::KMS::Alias" => self.delete_kms_alias(&resource.physical_id),
1983 "AWS::ECR::Repository" => self.delete_ecr_repository(&resource.physical_id),
1984 "AWS::ECR::RepositoryPolicy" => {
1985 self.delete_ecr_repository_policy(&resource.physical_id)
1986 }
1987 "AWS::ECR::LifecyclePolicy" => self.delete_ecr_lifecycle_policy(&resource.physical_id),
1988 "AWS::ECR::RegistryPolicy" => self.delete_ecr_registry_policy(),
1989 "AWS::ECR::ReplicationConfiguration" => self.delete_ecr_replication_configuration(),
1990 "AWS::ECR::RegistryScanningConfiguration" => {
1991 self.delete_ecr_registry_scanning_configuration()
1992 }
1993 "AWS::ECR::PullThroughCacheRule" => {
1994 self.delete_ecr_pull_through_cache_rule(&resource.physical_id)
1995 }
1996 "AWS::CloudWatch::Alarm" => self.delete_cloudwatch_alarm(&resource.physical_id),
1997 "AWS::CloudWatch::Dashboard" => self.delete_cloudwatch_dashboard(&resource.physical_id),
1998 "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1999 self.delete_elbv2_load_balancer(&resource.physical_id)
2000 }
2001 "AWS::ElasticLoadBalancingV2::TargetGroup" => {
2002 self.delete_elbv2_target_group(&resource.physical_id)
2003 }
2004 "AWS::ElasticLoadBalancingV2::Listener" => {
2005 self.delete_elbv2_listener(&resource.physical_id)
2006 }
2007 "AWS::ElasticLoadBalancingV2::ListenerRule" => {
2008 self.delete_elbv2_listener_rule(&resource.physical_id)
2009 }
2010 "AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
2011 self.delete_elbv2_listener_certificate(&resource.physical_id)
2012 }
2013 "AWS::ElasticLoadBalancingV2::TrustStore" => {
2014 self.delete_elbv2_trust_store(&resource.physical_id)
2015 }
2016 "AWS::Organizations::Organization" => self.delete_organization(&resource.physical_id),
2017 "AWS::Organizations::OrganizationalUnit" => {
2018 self.delete_organization_unit(&resource.physical_id)
2019 }
2020 "AWS::Organizations::Account" => {
2021 self.delete_organization_account(&resource.physical_id)
2022 }
2023 "AWS::Organizations::Policy" => self.delete_organization_policy(&resource.physical_id),
2024 "AWS::Organizations::ResourcePolicy" => {
2025 self.delete_organization_resource_policy(&resource.physical_id)
2026 }
2027 "AWS::Cognito::UserPool" => self.delete_cognito_user_pool(&resource.physical_id),
2028 "AWS::Cognito::UserPoolClient" => {
2029 self.delete_cognito_user_pool_client(&resource.physical_id)
2030 }
2031 "AWS::Cognito::UserPoolDomain" => {
2032 self.delete_cognito_user_pool_domain(&resource.physical_id)
2033 }
2034 "AWS::Cognito::IdentityPool" => {
2035 self.delete_cognito_identity_pool(&resource.physical_id)
2036 }
2037 "AWS::Cognito::IdentityPoolRoleAttachment" => {
2038 self.delete_cognito_identity_pool_role_attachment(&resource.physical_id)
2039 }
2040 "AWS::RDS::DBSubnetGroup" => self.delete_rds_subnet_group(&resource.physical_id),
2041 "AWS::RDS::DBParameterGroup" => self.delete_rds_parameter_group(&resource.physical_id),
2042 "AWS::RDS::DBClusterParameterGroup" => {
2043 self.delete_rds_cluster_parameter_group(&resource.physical_id)
2044 }
2045 "AWS::RDS::OptionGroup" => self.delete_rds_option_group(&resource.physical_id),
2046 "AWS::RDS::EventSubscription" => {
2047 self.delete_rds_event_subscription(&resource.physical_id)
2048 }
2049 "AWS::RDS::DBSecurityGroup" => self.delete_rds_security_group(&resource.physical_id),
2050 "AWS::RDS::DBProxy" => self.delete_rds_db_proxy(&resource.physical_id),
2051 "AWS::RDS::DBInstance" => self.delete_rds_db_instance(&resource.physical_id),
2052 "AWS::RDS::DBCluster" => self.delete_rds_db_cluster(&resource.physical_id),
2053 "AWS::EC2::Instance" => {
2054 self.pending_container_teardowns.lock().push(
2058 ContainerTeardownIntent::Ec2Instance {
2059 instance_id: resource.physical_id.clone(),
2060 },
2061 );
2062 Ok(())
2063 }
2064 "AWS::EC2::VPC"
2065 | "AWS::EC2::Subnet"
2066 | "AWS::EC2::SecurityGroup"
2067 | "AWS::EC2::InternetGateway"
2068 | "AWS::EC2::RouteTable" => {
2069 self.delete_ec2_resource(&resource.resource_type, &resource.physical_id)
2070 }
2071 "AWS::AutoScaling::LaunchConfiguration" | "AWS::AutoScaling::AutoScalingGroup" => {
2072 self.delete_autoscaling(&resource.resource_type, &resource.physical_id);
2073 Ok(())
2074 }
2075 "AWS::Batch::ComputeEnvironment"
2076 | "AWS::Batch::JobQueue"
2077 | "AWS::Batch::JobDefinition"
2078 | "AWS::Batch::SchedulingPolicy" => {
2079 self.delete_batch(&resource.resource_type, &resource.physical_id);
2080 Ok(())
2081 }
2082 "AWS::Pipes::Pipe" => {
2083 self.delete_pipes_pipe(&resource.physical_id);
2084 Ok(())
2085 }
2086 "AWS::CodeArtifact::Domain" => {
2087 self.delete_codeartifact_domain(&resource.physical_id);
2088 Ok(())
2089 }
2090 "AWS::CodeArtifact::Repository" => {
2091 self.delete_codeartifact_repository(&resource.physical_id);
2092 Ok(())
2093 }
2094 "AWS::AmazonMQ::Broker" => {
2095 self.delete_mq_broker(&resource.physical_id);
2096 Ok(())
2097 }
2098 "AWS::AmazonMQ::Configuration" => {
2099 self.delete_mq_configuration(&resource.physical_id);
2100 Ok(())
2101 }
2102 "AWS::AmazonMQ::ConfigurationAssociation" => Ok(()),
2103 "AWS::MSK::Cluster" | "AWS::MSK::ServerlessCluster" => {
2104 self.delete_msk_cluster(&resource.physical_id);
2105 Ok(())
2106 }
2107 "AWS::MSK::Configuration" => {
2108 self.delete_msk_configuration(&resource.physical_id);
2109 Ok(())
2110 }
2111 "AWS::MSK::ClusterPolicy" => {
2112 self.delete_msk_cluster_policy(&resource.physical_id);
2113 Ok(())
2114 }
2115 "AWS::MSK::BatchScramSecret" => {
2116 self.delete_msk_batch_scram_secret(&resource.physical_id);
2117 Ok(())
2118 }
2119 "AWS::MSK::VpcConnection" => {
2120 self.delete_msk_vpc_connection(&resource.physical_id);
2121 Ok(())
2122 }
2123 "AWS::MSK::Replicator" => {
2124 self.delete_msk_replicator(&resource.physical_id);
2125 Ok(())
2126 }
2127 "AWS::KinesisAnalyticsV2::Application" => {
2128 self.delete_ka2_application(&resource.physical_id);
2129 Ok(())
2130 }
2131 "AWS::KinesisAnalyticsV2::ApplicationOutput" => {
2132 self.delete_ka2_application_output(resource);
2133 Ok(())
2134 }
2135 "AWS::KinesisAnalyticsV2::ApplicationReferenceDataSource" => {
2136 self.delete_ka2_reference_data_source(resource);
2137 Ok(())
2138 }
2139 "AWS::KinesisAnalyticsV2::ApplicationCloudWatchLoggingOption" => {
2140 self.delete_ka2_cloudwatch_logging_option(resource);
2141 Ok(())
2142 }
2143 "AWS::CodeCommit::Repository" => {
2144 self.delete_codecommit_repository(&resource.physical_id);
2145 Ok(())
2146 }
2147 "AWS::EFS::FileSystem" => {
2148 self.delete_efs_file_system(&resource.physical_id);
2149 Ok(())
2150 }
2151 "AWS::EFS::MountTarget" => {
2152 self.delete_efs_mount_target(&resource.physical_id);
2153 Ok(())
2154 }
2155 "AWS::EFS::AccessPoint" => {
2156 self.delete_efs_access_point(&resource.physical_id);
2157 Ok(())
2158 }
2159 "AWS::ElasticBeanstalk::Application" => {
2160 self.delete_eb_application(&resource.physical_id);
2161 Ok(())
2162 }
2163 "AWS::ElasticBeanstalk::ApplicationVersion" => {
2164 self.delete_eb_application_version(resource);
2165 Ok(())
2166 }
2167 "AWS::ElasticBeanstalk::Environment" => {
2168 self.delete_eb_environment(resource);
2169 Ok(())
2170 }
2171 "AWS::ElasticBeanstalk::ConfigurationTemplate" => {
2172 self.delete_eb_configuration_template(resource);
2173 Ok(())
2174 }
2175 "AWS::ECS::Cluster" => self.delete_ecs_cluster(&resource.physical_id),
2176 "AWS::ECS::TaskDefinition" => self.delete_ecs_task_definition(&resource.physical_id),
2177 "AWS::ECS::Service" => self.delete_ecs_service(&resource.physical_id),
2178 "AWS::ECS::CapacityProvider" => {
2179 self.delete_ecs_capacity_provider(&resource.physical_id)
2180 }
2181 "AWS::CertificateManager::Certificate" => {
2182 self.delete_acm_certificate(&resource.physical_id)
2183 }
2184 "AWS::CertificateManager::Account" => self.delete_acm_account(),
2185 "AWS::ACMPCA::CertificateAuthority" => {
2186 self.delete_acmpca_certificate_authority(&resource.physical_id)
2187 }
2188 "AWS::ACMPCA::Certificate" => Ok(()),
2189 "AWS::ACMPCA::CertificateAuthorityActivation" => Ok(()),
2190 "AWS::ACMPCA::Permission" => self.delete_acmpca_permission(&resource.physical_id),
2191 "AWS::Route53Resolver::ResolverEndpoint" => {
2192 self.delete_r53r_resolver_endpoint(&resource.physical_id)
2193 }
2194 "AWS::Route53Resolver::ResolverRule" => {
2195 self.delete_r53r_resolver_rule(&resource.physical_id)
2196 }
2197 "AWS::Route53Resolver::ResolverRuleAssociation" => {
2198 self.delete_r53r_rule_association(&resource.physical_id)
2199 }
2200 "AWS::Route53Resolver::ResolverQueryLoggingConfig" => {
2201 self.delete_r53r_query_log_config(&resource.physical_id)
2202 }
2203 "AWS::Route53Resolver::ResolverQueryLoggingConfigAssociation" => {
2204 self.delete_r53r_query_log_association(&resource.physical_id)
2205 }
2206 "AWS::Route53Resolver::FirewallDomainList" => {
2207 self.delete_r53r_firewall_domain_list(&resource.physical_id)
2208 }
2209 "AWS::Route53Resolver::FirewallRuleGroup" => {
2210 self.delete_r53r_firewall_rule_group(&resource.physical_id)
2211 }
2212 "AWS::Route53Resolver::FirewallRuleGroupAssociation" => {
2213 self.delete_r53r_firewall_rule_group_association(&resource.physical_id)
2214 }
2215 "AWS::Route53Resolver::FirewallConfig" => {
2216 self.delete_r53r_firewall_config(&resource.physical_id)
2217 }
2218 "AWS::Route53Resolver::ResolverConfig" => {
2219 self.delete_r53r_resolver_config(&resource.physical_id)
2220 }
2221 "AWS::Route53Resolver::ResolverDNSSECConfig" => {
2222 self.delete_r53r_dnssec_config(&resource.physical_id)
2223 }
2224 "AWS::Config::ConfigurationRecorder"
2225 | "AWS::Config::DeliveryChannel"
2226 | "AWS::Config::ConfigRule"
2227 | "AWS::Config::ConfigurationAggregator"
2228 | "AWS::Config::AggregationAuthorization"
2229 | "AWS::Config::ConformancePack"
2230 | "AWS::Config::OrganizationConfigRule" => {
2231 self.delete_config_resource(&resource.resource_type, &resource.physical_id)
2232 }
2233 "AWS::ElastiCache::ParameterGroup" => {
2234 self.delete_ec_parameter_group(&resource.physical_id)
2235 }
2236 "AWS::ElastiCache::SubnetGroup" => self.delete_ec_subnet_group(&resource.physical_id),
2237 "AWS::ElastiCache::SecurityGroup" => {
2238 self.delete_ec_security_group(&resource.physical_id)
2239 }
2240 "AWS::ElastiCache::User" => self.delete_ec_user(&resource.physical_id),
2241 "AWS::ElastiCache::UserGroup" => self.delete_ec_user_group(&resource.physical_id),
2242 "AWS::ElastiCache::CacheCluster" => self.delete_ec_cache_cluster(&resource.physical_id),
2243 "AWS::ElastiCache::ReplicationGroup" => {
2244 self.delete_ec_replication_group(&resource.physical_id)
2245 }
2246 "AWS::Route53::HostedZone" => self.delete_route53_hosted_zone(&resource.physical_id),
2247 "AWS::Route53::RecordSet" => {
2248 self.delete_route53_record_set(&resource.physical_id, &resource.attributes)
2249 }
2250 "AWS::Route53::HealthCheck" => self.delete_route53_health_check(&resource.physical_id),
2251 "AWS::Route53::DNSSEC" => self.delete_route53_dnssec(&resource.physical_id),
2252 "AWS::Route53::KeySigningKey" => {
2253 self.delete_route53_key_signing_key(&resource.physical_id)
2254 }
2255 "AWS::CloudFront::CloudFrontOriginAccessIdentity" => {
2256 self.delete_cf_origin_access_identity(&resource.physical_id)
2257 }
2258 "AWS::CloudFront::Distribution" => self.delete_cf_distribution(&resource.physical_id),
2259 "AWS::CloudFront::OriginAccessControl" => {
2260 self.delete_cf_origin_access_control(&resource.physical_id)
2261 }
2262 "AWS::CloudFront::PublicKey" => self.delete_cf_public_key(&resource.physical_id),
2263 "AWS::CloudFront::KeyGroup" => self.delete_cf_key_group(&resource.physical_id),
2264 "AWS::CloudFront::Function" => self.delete_cf_function(&resource.physical_id),
2265 "AWS::CloudFront::CachePolicy" => self.delete_cf_cache_policy(&resource.physical_id),
2266 "AWS::CloudFront::OriginRequestPolicy" => {
2267 self.delete_cf_origin_request_policy(&resource.physical_id)
2268 }
2269 "AWS::CloudFront::ResponseHeadersPolicy" => {
2270 self.delete_cf_response_headers_policy(&resource.physical_id)
2271 }
2272 "AWS::StepFunctions::StateMachine" => {
2273 self.delete_sfn_state_machine(&resource.physical_id)
2274 }
2275 "AWS::StepFunctions::Activity" => self.delete_sfn_activity(&resource.physical_id),
2276 "AWS::StepFunctions::StateMachineVersion" => {
2277 self.delete_sfn_version(&resource.physical_id)
2278 }
2279 "AWS::StepFunctions::StateMachineAlias" => self.delete_sfn_alias(&resource.physical_id),
2280 "AWS::WAFv2::WebACL" => self.delete_wafv2_web_acl(&resource.physical_id),
2281 "AWS::WAFv2::IPSet" => self.delete_wafv2_ip_set(&resource.physical_id),
2282 "AWS::WAFv2::RegexPatternSet" => {
2283 self.delete_wafv2_regex_pattern_set(&resource.physical_id)
2284 }
2285 "AWS::WAFv2::RuleGroup" => self.delete_wafv2_rule_group(&resource.physical_id),
2286 "AWS::WAFv2::LoggingConfiguration" => {
2287 self.delete_wafv2_logging_configuration(&resource.physical_id)
2288 }
2289 "AWS::WAFv2::WebACLAssociation" => {
2290 self.delete_wafv2_web_acl_association(&resource.physical_id)
2291 }
2292 "AWS::ApiGateway::RestApi" => self.delete_apigw_rest_api(&resource.physical_id),
2293 "AWS::ApiGateway::Resource" => {
2294 self.delete_apigw_resource(&resource.physical_id, &resource.attributes)
2295 }
2296 "AWS::ApiGateway::Method" => self.delete_apigw_method(&resource.physical_id),
2297 "AWS::ApiGateway::Deployment" => {
2298 self.delete_apigw_deployment(&resource.physical_id, &resource.attributes)
2299 }
2300 "AWS::ApiGateway::Stage" => {
2301 self.delete_apigw_stage(&resource.physical_id, &resource.attributes)
2302 }
2303 "AWS::ApiGateway::Authorizer" => {
2304 self.delete_apigw_authorizer(&resource.physical_id, &resource.attributes)
2305 }
2306 "AWS::ApiGateway::RequestValidator" => {
2307 self.delete_apigw_request_validator(&resource.physical_id, &resource.attributes)
2308 }
2309 "AWS::ApiGateway::Model" => {
2310 self.delete_apigw_model(&resource.physical_id, &resource.attributes)
2311 }
2312 "AWS::ApiGateway::GatewayResponse" => {
2313 self.delete_apigw_gateway_response(&resource.physical_id, &resource.attributes)
2314 }
2315 "AWS::ApiGateway::UsagePlan" => self.delete_apigw_usage_plan(&resource.physical_id),
2316 "AWS::ApiGateway::ApiKey" => self.delete_apigw_api_key(&resource.physical_id),
2317 "AWS::ApiGateway::UsagePlanKey" => {
2318 self.delete_apigw_usage_plan_key(&resource.physical_id, &resource.attributes)
2319 }
2320 "AWS::ApiGateway::DomainName" => self.delete_apigw_domain_name(&resource.physical_id),
2321 "AWS::ApiGateway::BasePathMapping" => {
2322 self.delete_apigw_base_path_mapping(&resource.physical_id, &resource.attributes)
2323 }
2324 "AWS::ApiGatewayV2::Api" => self.delete_apigwv2_api(&resource.physical_id),
2325 "AWS::ApiGatewayV2::Route" => {
2326 self.delete_apigwv2_route(&resource.physical_id, &resource.attributes)
2327 }
2328 "AWS::ApiGatewayV2::Integration" => {
2329 self.delete_apigwv2_integration(&resource.physical_id, &resource.attributes)
2330 }
2331 "AWS::ApiGatewayV2::IntegrationResponse" => self
2332 .delete_apigwv2_integration_response(&resource.physical_id, &resource.attributes),
2333 "AWS::ApiGatewayV2::RouteResponse" => {
2334 self.delete_apigwv2_route_response(&resource.physical_id, &resource.attributes)
2335 }
2336 "AWS::ApiGatewayV2::Stage" => {
2337 self.delete_apigwv2_stage(&resource.physical_id, &resource.attributes)
2338 }
2339 "AWS::ApiGatewayV2::Deployment" => {
2340 self.delete_apigwv2_deployment(&resource.physical_id, &resource.attributes)
2341 }
2342 "AWS::ApiGatewayV2::Authorizer" => {
2343 self.delete_apigwv2_authorizer(&resource.physical_id, &resource.attributes)
2344 }
2345 "AWS::ApiGatewayV2::DomainName" => {
2346 self.delete_apigwv2_domain_name(&resource.physical_id)
2347 }
2348 "AWS::ApiGatewayV2::ApiMapping" => {
2349 self.delete_apigwv2_api_mapping(&resource.physical_id, &resource.attributes)
2350 }
2351 "AWS::ApiGatewayV2::VpcLink" => self.delete_apigwv2_vpc_link(&resource.physical_id),
2352 "AWS::ApiGatewayV2::Model" => {
2353 self.delete_apigwv2_model(&resource.physical_id, &resource.attributes)
2354 }
2355 "AWS::SES::ConfigurationSet" => {
2356 self.delete_ses_configuration_set(&resource.physical_id)
2357 }
2358 "AWS::SES::ConfigurationSetEventDestination" => {
2359 self.delete_ses_event_destination(&resource.physical_id, &resource.attributes)
2360 }
2361 "AWS::SES::EmailIdentity" => self.delete_ses_email_identity(&resource.physical_id),
2362 "AWS::SES::Template" => self.delete_ses_template(&resource.physical_id),
2363 "AWS::SES::ContactList" => self.delete_ses_contact_list(&resource.physical_id),
2364 "AWS::SES::DedicatedIpPool" => self.delete_ses_dedicated_ip_pool(&resource.physical_id),
2365 "AWS::SES::ReceiptRule" => {
2366 self.delete_ses_receipt_rule(&resource.physical_id, &resource.attributes)
2367 }
2368 "AWS::SES::ReceiptRuleSet" => self.delete_ses_receipt_rule_set(&resource.physical_id),
2369 "AWS::SES::ReceiptFilter" => self.delete_ses_receipt_filter(&resource.physical_id),
2370 "AWS::SES::VdmAttributes" => Ok(()),
2371 "AWS::SecretsManager::RotationSchedule" => {
2372 self.delete_secrets_manager_rotation_schedule(&resource.physical_id)
2373 }
2374 "AWS::SecretsManager::ResourcePolicy" => {
2375 self.delete_secrets_manager_resource_policy(&resource.physical_id)
2376 }
2377 "AWS::SecretsManager::SecretTargetAttachment" => Ok(()),
2378 "AWS::ApplicationAutoScaling::ScalableTarget" => self
2379 .delete_application_autoscaling_scalable_target(
2380 &resource.physical_id,
2381 &resource.attributes,
2382 ),
2383 "AWS::ApplicationAutoScaling::ScalingPolicy" => self
2384 .delete_application_autoscaling_scaling_policy(
2385 &resource.physical_id,
2386 &resource.attributes,
2387 ),
2388 "AWS::Athena::DataCatalog" => self.delete_athena_data_catalog(&resource.physical_id),
2389 "AWS::Athena::NamedQuery" => self.delete_athena_named_query(&resource.physical_id),
2390 "AWS::Athena::WorkGroup" => self.delete_athena_work_group(&resource.physical_id),
2391 "AWS::Athena::PreparedStatement" => {
2392 self.delete_athena_prepared_statement(&resource.physical_id, &resource.attributes)
2393 }
2394 "AWS::KinesisFirehose::DeliveryStream" => {
2395 self.delete_firehose_delivery_stream(&resource.physical_id)
2396 }
2397 "AWS::Glue::Database" => self.delete_glue_database(&resource.physical_id),
2398 "AWS::CloudFormation::Stack" => self.delete_cloudformation_stack(&resource.physical_id),
2399 "AWS::Glue::Table" => self.delete_glue_table(&resource.physical_id),
2400 "AWS::Glue::Partition" => {
2401 self.delete_glue_partition(&resource.physical_id, &resource.attributes)
2402 }
2403 "AWS::EKS::Cluster" => self.delete_eks_cluster(&resource.physical_id),
2404 "AWS::EKS::Nodegroup" => self.delete_eks_nodegroup(&resource.physical_id),
2405 "AWS::EKS::FargateProfile" => self.delete_eks_fargate_profile(&resource.physical_id),
2406 "AWS::EKS::Addon" => self.delete_eks_addon(&resource.physical_id),
2407 "AWS::EKS::AccessEntry" => self.delete_eks_access_entry(&resource.physical_id),
2408 "AWS::EKS::IdentityProviderConfig" => {
2409 self.delete_eks_identity_provider_config(&resource.physical_id)
2410 }
2411 "AWS::EKS::PodIdentityAssociation" => {
2412 self.delete_eks_pod_identity_association(&resource.physical_id)
2413 }
2414 "AWS::ServiceDiscovery::HttpNamespace"
2415 | "AWS::ServiceDiscovery::PublicDnsNamespace"
2416 | "AWS::ServiceDiscovery::PrivateDnsNamespace" => {
2417 self.delete_sd_namespace(&resource.physical_id)
2418 }
2419 "AWS::ServiceDiscovery::Service" => self.delete_sd_service(&resource.physical_id),
2420 "AWS::ServiceDiscovery::Instance" => {
2421 self.delete_sd_instance(&resource.physical_id, &resource.attributes)
2422 }
2423 t if t.starts_with("Custom::") || t == "AWS::CloudFormation::CustomResource" => {
2424 self.delete_custom_resource(resource)
2425 }
2426 _ => Ok(()),
2430 }
2431 }
2432
2433 fn read_s3_object_bytes(&self, bucket: &str, key: &str) -> Result<Vec<u8>, String> {
2436 let mut accounts = self.s3_state.write();
2437 let state = accounts.get_or_create(&self.account_id);
2438 let body_ref = {
2439 let b = state
2440 .buckets
2441 .get(bucket)
2442 .ok_or_else(|| format!("S3 bucket {bucket} does not exist"))?;
2443 let object = b
2444 .objects
2445 .get(key)
2446 .ok_or_else(|| format!("S3 object s3://{bucket}/{key} does not exist"))?;
2447 object.body.clone()
2448 };
2449 state
2452 .read_body(&body_ref)
2453 .map(|b| b.to_vec())
2454 .map_err(|e| format!("S3 read failed: {e}"))
2455 }
2456
2457 fn read_s3_object_version_bytes(
2463 &self,
2464 bucket: &str,
2465 key: &str,
2466 version_id: &str,
2467 ) -> Result<Vec<u8>, String> {
2468 let mut accounts = self.s3_state.write();
2469 let state = accounts.get_or_create(&self.account_id);
2470 let body_ref = {
2471 let b = state
2472 .buckets
2473 .get(bucket)
2474 .ok_or_else(|| format!("S3 bucket {bucket} does not exist"))?;
2475 let from_current = b
2476 .objects
2477 .get(key)
2478 .filter(|o| o.version_id.as_deref() == Some(version_id))
2479 .map(|o| o.body.clone());
2480 from_current
2481 .or_else(|| {
2482 b.object_versions.get(key).and_then(|versions| {
2483 versions
2484 .iter()
2485 .find(|o| o.version_id.as_deref() == Some(version_id))
2486 .map(|o| o.body.clone())
2487 })
2488 })
2489 .ok_or_else(|| {
2490 format!("S3 object s3://{bucket}/{key} version {version_id} does not exist")
2491 })?
2492 };
2493 state
2494 .read_body(&body_ref)
2495 .map(|b| b.to_vec())
2496 .map_err(|e| format!("S3 read failed: {e}"))
2497 }
2498
2499 fn append_lambda_permission_statement(
2505 &self,
2506 function_name: &str,
2507 statement_id: &str,
2508 props: &serde_json::Value,
2509 ) -> Result<String, String> {
2510 let action = props
2511 .get("Action")
2512 .and_then(|v| v.as_str())
2513 .ok_or_else(|| "Action is required".to_string())?
2514 .to_string();
2515 let principal = props
2516 .get("Principal")
2517 .and_then(|v| v.as_str())
2518 .ok_or_else(|| "Principal is required".to_string())?
2519 .to_string();
2520 let source_arn = props
2521 .get("SourceArn")
2522 .and_then(|v| v.as_str())
2523 .map(|s| s.to_string());
2524 let source_account = props
2525 .get("SourceAccount")
2526 .and_then(|v| v.as_str())
2527 .map(|s| s.to_string());
2528 let event_source_token = props
2529 .get("EventSourceToken")
2530 .and_then(|v| v.as_str())
2531 .map(|s| s.to_string());
2532 let function_url_auth_type = props
2533 .get("FunctionUrlAuthType")
2534 .and_then(|v| v.as_str())
2535 .map(|s| s.to_string());
2536 let principal_org_id = props
2537 .get("PrincipalOrgID")
2538 .and_then(|v| v.as_str())
2539 .map(|s| s.to_string());
2540
2541 let mut accounts = self.lambda_state.write();
2542 let state = accounts.get_or_create(&self.account_id);
2543 let func = state.functions.get_mut(function_name).ok_or_else(|| {
2544 format!(
2545 "Function {function_name} does not exist yet — retry once it has been provisioned"
2546 )
2547 })?;
2548
2549 let mut doc: serde_json::Value = func
2550 .policy
2551 .as_deref()
2552 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
2553 .filter(|v| v.is_object())
2554 .unwrap_or_else(|| serde_json::json!({"Version": "2012-10-17", "Statement": []}));
2555 if !doc.get("Statement").map(|s| s.is_array()).unwrap_or(false) {
2556 doc["Statement"] = serde_json::json!([]);
2557 }
2558 let principal_value =
2559 if principal.ends_with(".amazonaws.com") || principal.contains(".amazon") {
2560 serde_json::json!({ "Service": principal })
2561 } else {
2562 serde_json::json!({ "AWS": principal })
2563 };
2564 let mut arn_like = serde_json::Map::new();
2565 let mut string_equals = serde_json::Map::new();
2566 if let Some(src) = source_arn {
2567 arn_like.insert("AWS:SourceArn".to_string(), serde_json::Value::String(src));
2568 }
2569 if let Some(acct) = source_account {
2570 string_equals.insert(
2571 "AWS:SourceAccount".to_string(),
2572 serde_json::Value::String(acct),
2573 );
2574 }
2575 if let Some(token) = event_source_token {
2576 string_equals.insert(
2577 "lambda:EventSourceToken".to_string(),
2578 serde_json::Value::String(token),
2579 );
2580 }
2581 if let Some(auth) = function_url_auth_type {
2582 string_equals.insert(
2583 "lambda:FunctionUrlAuthType".to_string(),
2584 serde_json::Value::String(auth),
2585 );
2586 }
2587 if let Some(org) = principal_org_id {
2588 string_equals.insert(
2589 "aws:PrincipalOrgID".to_string(),
2590 serde_json::Value::String(org),
2591 );
2592 }
2593 let mut conditions = serde_json::Map::new();
2594 if !arn_like.is_empty() {
2595 conditions.insert("ArnLike".to_string(), serde_json::Value::Object(arn_like));
2596 }
2597 if !string_equals.is_empty() {
2598 conditions.insert(
2599 "StringEquals".to_string(),
2600 serde_json::Value::Object(string_equals),
2601 );
2602 }
2603
2604 let mut statement = serde_json::Map::new();
2605 statement.insert(
2606 "Sid".to_string(),
2607 serde_json::Value::String(statement_id.to_string()),
2608 );
2609 statement.insert(
2610 "Effect".to_string(),
2611 serde_json::Value::String("Allow".to_string()),
2612 );
2613 statement.insert("Principal".to_string(), principal_value);
2614 statement.insert("Action".to_string(), serde_json::Value::String(action));
2615 statement.insert(
2616 "Resource".to_string(),
2617 serde_json::Value::String(func.function_arn.clone()),
2618 );
2619 if !conditions.is_empty() {
2620 statement.insert(
2621 "Condition".to_string(),
2622 serde_json::Value::Object(conditions),
2623 );
2624 }
2625 doc["Statement"]
2626 .as_array_mut()
2627 .unwrap()
2628 .push(serde_json::Value::Object(statement));
2629 func.policy = Some(doc.to_string());
2630 Ok(func.function_arn.clone())
2631 }
2632
2633 fn get_att_elbv2_load_balancer(&self, physical_id: &str, attribute: &str) -> Option<String> {
2636 let mut accounts = self.elbv2_state.write();
2637 let state = accounts.get_or_create(&self.account_id);
2638 let lb = state.load_balancers.get(physical_id)?;
2639 let lb_full = lb
2640 .arn
2641 .rsplit("loadbalancer/")
2642 .next()
2643 .unwrap_or("")
2644 .to_string();
2645 match attribute {
2646 "Arn" | "LoadBalancerArn" => Some(lb.arn.clone()),
2647 "DNSName" => Some(lb.dns_name.clone()),
2648 "CanonicalHostedZoneID" => Some(lb.canonical_hosted_zone_id.clone()),
2649 "LoadBalancerFullName" => Some(lb_full),
2650 "LoadBalancerName" => Some(lb.name.clone()),
2651 "SecurityGroups" => Some(lb.security_groups.join(",")),
2652 _ => None,
2653 }
2654 }
2655
2656 fn get_att_elbv2_target_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
2658 let mut accounts = self.elbv2_state.write();
2659 let state = accounts.get_or_create(&self.account_id);
2660 let tg = state.target_groups.get(physical_id)?;
2661 let tg_full = tg
2662 .arn
2663 .rsplit("targetgroup/")
2664 .next()
2665 .map(|s| format!("targetgroup/{s}"))
2666 .unwrap_or_default();
2667 match attribute {
2668 "TargetGroupArn" => Some(tg.arn.clone()),
2669 "TargetGroupName" => Some(tg.name.clone()),
2670 "TargetGroupFullName" => Some(tg_full),
2671 "LoadBalancerArns" => Some(tg.load_balancer_arns.join(",")),
2672 _ => None,
2673 }
2674 }
2675
2676 fn get_att_elbv2_listener(&self, physical_id: &str, attribute: &str) -> Option<String> {
2678 let mut accounts = self.elbv2_state.write();
2679 let state = accounts.get_or_create(&self.account_id);
2680 let listener = state.listeners.get(physical_id)?;
2681 match attribute {
2682 "Arn" | "ListenerArn" => Some(listener.arn.clone()),
2683 _ => None,
2684 }
2685 }
2686
2687 fn get_att_elbv2_listener_rule(&self, physical_id: &str, attribute: &str) -> Option<String> {
2689 let mut accounts = self.elbv2_state.write();
2690 let state = accounts.get_or_create(&self.account_id);
2691 let rule = state.rules.get(physical_id)?;
2692 match attribute {
2693 "RuleArn" => Some(rule.arn.clone()),
2694 "IsDefault" => Some(rule.is_default.to_string()),
2695 _ => None,
2696 }
2697 }
2698
2699 fn get_att_elbv2_trust_store(&self, physical_id: &str, attribute: &str) -> Option<String> {
2701 let mut accounts = self.elbv2_state.write();
2702 let state = accounts.get_or_create(&self.account_id);
2703 let ts = state.trust_stores.get(physical_id)?;
2704 match attribute {
2705 "TrustStoreArn" => Some(ts.arn.clone()),
2706 "Name" => Some(ts.name.clone()),
2707 "Status" => Some(ts.status.clone()),
2708 "NumberOfCaCertificates" => Some(ts.number_of_ca_certificates.to_string()),
2709 "TotalRevokedEntries" => Some(ts.total_revoked_entries.to_string()),
2710 _ => None,
2711 }
2712 }
2713
2714 fn invoke_lambda_sync(&self, function_arn: &str, payload: &str) -> Result<(), String> {
2717 let delivery = self.delivery.clone();
2718 let function_arn = function_arn.to_string();
2719 let payload = payload.to_string();
2720 std::thread::scope(|s| {
2721 s.spawn(|| {
2722 let rt = tokio::runtime::Builder::new_current_thread()
2723 .enable_all()
2724 .build()
2725 .map_err(|e| format!("Failed to create runtime: {e}"))?;
2726 rt.block_on(async {
2727 match delivery.invoke_lambda(&function_arn, &payload).await {
2728 Some(Ok(_)) => {
2729 tracing::info!(
2730 "Custom resource Lambda {} invoked successfully",
2731 function_arn
2732 );
2733 Ok(())
2734 }
2735 Some(Err(e)) => {
2736 tracing::warn!(
2737 "Custom resource Lambda {} invocation failed: {e}",
2738 function_arn
2739 );
2740 Err(format!("Lambda invocation failed: {e}"))
2741 }
2742 None => {
2743 tracing::warn!(
2744 "No Lambda delivery configured; skipping custom resource invocation for {}",
2745 function_arn
2746 );
2747 Ok(())
2748 }
2749 }
2750 })
2751 })
2752 .join()
2753 .map_err(|_| "Lambda invocation thread panicked".to_string())?
2754 })
2755 }
2756
2757 fn create_custom_resource(&self, resource: &ResourceDefinition) -> Result<String, String> {
2758 let props = &resource.properties;
2759 let service_token = props
2760 .get("ServiceToken")
2761 .and_then(|v| v.as_str())
2762 .ok_or("Custom resource requires ServiceToken property")?;
2763
2764 let request_id = Uuid::new_v4().to_string();
2765
2766 let event = serde_json::json!({
2768 "RequestType": "Create",
2769 "ServiceToken": service_token,
2770 "StackId": self.stack_id,
2771 "RequestId": request_id,
2772 "ResourceType": resource.resource_type,
2773 "LogicalResourceId": resource.logical_id,
2774 "ResourceProperties": props,
2775 });
2776
2777 let payload = serde_json::to_string(&event).map_err(|e| e.to_string())?;
2778 if self.defer_custom_invokes {
2779 self.pending_custom_invokes.lock().push(CustomInvokeIntent {
2784 service_token: service_token.to_string(),
2785 payload,
2786 });
2787 } else {
2788 self.invoke_lambda_sync(service_token, &payload)?;
2789 }
2790
2791 let physical_id = format!("{}-{}", resource.logical_id, &request_id[..8]);
2794 Ok(physical_id)
2795 }
2796
2797 fn delete_custom_resource(&self, resource: &StackResource) -> Result<(), String> {
2798 let service_token = match &resource.service_token {
2799 Some(token) => token.clone(),
2800 None => {
2801 return Ok(());
2803 }
2804 };
2805
2806 let request_id = Uuid::new_v4().to_string();
2807
2808 let event = serde_json::json!({
2809 "RequestType": "Delete",
2810 "ServiceToken": service_token,
2811 "StackId": self.stack_id,
2812 "RequestId": request_id,
2813 "ResourceType": resource.resource_type,
2814 "LogicalResourceId": resource.logical_id,
2815 "PhysicalResourceId": resource.physical_id,
2816 });
2817
2818 let payload = serde_json::to_string(&event).map_err(|e| e.to_string())?;
2819
2820 if self.defer_custom_invokes {
2821 self.pending_custom_invokes.lock().push(CustomInvokeIntent {
2824 service_token,
2825 payload,
2826 });
2827 } else if let Err(e) = self.invoke_lambda_sync(&service_token, &payload) {
2828 tracing::warn!(
2830 "Custom resource delete Lambda invocation failed for {}: {e}",
2831 resource.logical_id
2832 );
2833 }
2834 Ok(())
2835 }
2836
2837 fn parse_athena_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
2840 let mut out = BTreeMap::new();
2841 let Some(arr) = value.and_then(|v| v.as_array()) else {
2842 return out;
2843 };
2844 for tag in arr {
2845 if let (Some(k), Some(v)) = (
2846 tag.get("Key").and_then(|v| v.as_str()),
2847 tag.get("Value").and_then(|v| v.as_str()),
2848 ) {
2849 out.insert(k.to_string(), v.to_string());
2850 }
2851 }
2852 out
2853 }
2854
2855 fn fetch_template_from_url(&self, url: &str) -> Result<String, String> {
2856 if let Some(rest) = url.strip_prefix("s3://") {
2857 let parts: Vec<&str> = rest.splitn(2, '/').collect();
2858 if parts.len() != 2 {
2859 return Err("Invalid s3:// URL".to_string());
2860 }
2861 return self.fetch_s3_template(parts[0], parts[1]);
2862 }
2863
2864 if let Some(rest) = url.strip_prefix("https://s3.amazonaws.com/") {
2865 let parts: Vec<&str> = rest.splitn(2, '/').collect();
2866 if parts.len() != 2 {
2867 return Err("Invalid S3 HTTPS URL".to_string());
2868 }
2869 return self.fetch_s3_template(parts[0], parts[1]);
2870 }
2871
2872 if let Some(host_rest) = url.strip_prefix("https://") {
2873 if let Some(slash_pos) = host_rest.find('/') {
2874 let host = &host_rest[..slash_pos];
2875 let key = &host_rest[slash_pos + 1..];
2876 if let Some(bucket) = host.strip_suffix(".s3.amazonaws.com") {
2877 return self.fetch_s3_template(bucket, key);
2878 }
2879 if host.contains(".s3.") && host.ends_with(".amazonaws.com") {
2880 let bucket = host.split(".s3.").next().unwrap_or("");
2881 if !bucket.is_empty() {
2882 return self.fetch_s3_template(bucket, key);
2883 }
2884 }
2885 }
2886 }
2887
2888 Err(format!("Unsupported TemplateURL: {url}"))
2889 }
2890
2891 fn fetch_s3_template(&self, bucket: &str, key: &str) -> Result<String, String> {
2892 let mut s3_accounts = self.s3_state.write();
2893 let s3_state = s3_accounts.get_or_create(&self.account_id);
2894 let bucket_obj = s3_state
2895 .buckets
2896 .get(bucket)
2897 .ok_or_else(|| format!("S3 bucket not found: {bucket}"))?;
2898 let obj = bucket_obj
2899 .objects
2900 .get(key)
2901 .ok_or_else(|| format!("S3 object not found: {bucket}/{key}"))?;
2902 let bytes = s3_state
2903 .read_body(&obj.body)
2904 .map_err(|e| format!("Failed to read S3 object body: {e}"))?;
2905 String::from_utf8(bytes.to_vec()).map_err(|e| format!("S3 object is not valid UTF-8: {e}"))
2906 }
2907}
2908
2909fn generate_secret_string_payload(gen: &serde_json::Value) -> Result<String, String> {
2918 let length = gen
2919 .get("PasswordLength")
2920 .and_then(|v| v.as_i64())
2921 .unwrap_or(32) as usize;
2922 let exclude_lowercase = gen
2923 .get("ExcludeLowercase")
2924 .and_then(|v| v.as_bool())
2925 .unwrap_or(false);
2926 let exclude_uppercase = gen
2927 .get("ExcludeUppercase")
2928 .and_then(|v| v.as_bool())
2929 .unwrap_or(false);
2930 let exclude_numbers = gen
2931 .get("ExcludeNumbers")
2932 .and_then(|v| v.as_bool())
2933 .unwrap_or(false);
2934 let exclude_punctuation = gen
2935 .get("ExcludePunctuation")
2936 .and_then(|v| v.as_bool())
2937 .unwrap_or(false);
2938 let include_space = gen
2939 .get("IncludeSpace")
2940 .and_then(|v| v.as_bool())
2941 .unwrap_or(false);
2942 let exclude_chars = gen
2943 .get("ExcludeCharacters")
2944 .and_then(|v| v.as_str())
2945 .unwrap_or("")
2946 .to_string();
2947
2948 let lowercase = "abcdefghijklmnopqrstuvwxyz";
2949 let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
2950 let digits = "0123456789";
2951 let punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
2952
2953 let mut pool = String::new();
2954 if !exclude_lowercase {
2955 pool.extend(lowercase.chars().filter(|c| !exclude_chars.contains(*c)));
2956 }
2957 if !exclude_uppercase {
2958 pool.extend(uppercase.chars().filter(|c| !exclude_chars.contains(*c)));
2959 }
2960 if !exclude_numbers {
2961 pool.extend(digits.chars().filter(|c| !exclude_chars.contains(*c)));
2962 }
2963 if !exclude_punctuation {
2964 pool.extend(punctuation.chars().filter(|c| !exclude_chars.contains(*c)));
2965 }
2966 if include_space && !exclude_chars.contains(' ') {
2967 pool.push(' ');
2968 }
2969 if pool.is_empty() {
2970 return Err("GenerateSecretString character pool is empty".to_string());
2971 }
2972
2973 let pool_chars: Vec<char> = pool.chars().collect();
2974 let mut password = String::with_capacity(length);
2975 let mut counter: u64 = std::time::SystemTime::now()
2976 .duration_since(std::time::UNIX_EPOCH)
2977 .map(|d| d.as_nanos() as u64)
2978 .unwrap_or(0);
2979 while password.len() < length {
2980 counter = counter.wrapping_add(0x9E3779B97F4A7C15);
2983 let mut z = counter;
2984 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
2985 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
2986 z ^= z >> 31;
2987 let idx = (z as usize) % pool_chars.len();
2988 password.push(pool_chars[idx]);
2989 }
2990
2991 let template = gen.get("SecretStringTemplate").and_then(|v| v.as_str());
2992 let key = gen.get("GenerateStringKey").and_then(|v| v.as_str());
2993 match (template, key) {
2994 (Some(tmpl), Some(k)) => {
2995 let mut value: serde_json::Value = serde_json::from_str(tmpl)
2996 .map_err(|e| format!("SecretStringTemplate is not valid JSON: {e}"))?;
2997 if let Some(obj) = value.as_object_mut() {
2998 obj.insert(k.to_string(), serde_json::Value::String(password));
2999 Ok(value.to_string())
3000 } else {
3001 Err("SecretStringTemplate must be a JSON object".to_string())
3002 }
3003 }
3004 _ => Ok(password),
3005 }
3006}
3007
3008fn parse_ses_receipt_action(value: &serde_json::Value) -> Option<SesReceiptAction> {
3009 let obj = value.as_object()?;
3010 if let Some(s3) = obj.get("S3Action").and_then(|v| v.as_object()) {
3011 let bucket_name = s3.get("BucketName").and_then(|v| v.as_str())?.to_string();
3012 return Some(SesReceiptAction::S3 {
3013 bucket_name,
3014 object_key_prefix: s3
3015 .get("ObjectKeyPrefix")
3016 .and_then(|v| v.as_str())
3017 .map(String::from),
3018 topic_arn: s3
3019 .get("TopicArn")
3020 .and_then(|v| v.as_str())
3021 .map(String::from),
3022 kms_key_arn: s3
3023 .get("KmsKeyArn")
3024 .and_then(|v| v.as_str())
3025 .map(String::from),
3026 });
3027 }
3028 if let Some(sns) = obj.get("SNSAction").and_then(|v| v.as_object()) {
3029 return Some(SesReceiptAction::Sns {
3030 topic_arn: sns.get("TopicArn").and_then(|v| v.as_str())?.to_string(),
3031 encoding: sns
3032 .get("Encoding")
3033 .and_then(|v| v.as_str())
3034 .map(String::from),
3035 });
3036 }
3037 if let Some(la) = obj.get("LambdaAction").and_then(|v| v.as_object()) {
3038 return Some(SesReceiptAction::Lambda {
3039 function_arn: la.get("FunctionArn").and_then(|v| v.as_str())?.to_string(),
3040 invocation_type: la
3041 .get("InvocationType")
3042 .and_then(|v| v.as_str())
3043 .map(String::from),
3044 topic_arn: la
3045 .get("TopicArn")
3046 .and_then(|v| v.as_str())
3047 .map(String::from),
3048 });
3049 }
3050 if let Some(b) = obj.get("BounceAction").and_then(|v| v.as_object()) {
3051 return Some(SesReceiptAction::Bounce {
3052 smtp_reply_code: b
3053 .get("SmtpReplyCode")
3054 .and_then(|v| v.as_str())
3055 .unwrap_or("550")
3056 .to_string(),
3057 message: b
3058 .get("Message")
3059 .and_then(|v| v.as_str())
3060 .unwrap_or("")
3061 .to_string(),
3062 sender: b
3063 .get("Sender")
3064 .and_then(|v| v.as_str())
3065 .unwrap_or("")
3066 .to_string(),
3067 status_code: b
3068 .get("StatusCode")
3069 .and_then(|v| v.as_str())
3070 .map(String::from),
3071 topic_arn: b.get("TopicArn").and_then(|v| v.as_str()).map(String::from),
3072 });
3073 }
3074 if let Some(ah) = obj.get("AddHeaderAction").and_then(|v| v.as_object()) {
3075 return Some(SesReceiptAction::AddHeader {
3076 header_name: ah.get("HeaderName").and_then(|v| v.as_str())?.to_string(),
3077 header_value: ah.get("HeaderValue").and_then(|v| v.as_str())?.to_string(),
3078 });
3079 }
3080 if let Some(s) = obj.get("StopAction").and_then(|v| v.as_object()) {
3081 return Some(SesReceiptAction::Stop {
3082 scope: s
3083 .get("Scope")
3084 .and_then(|v| v.as_str())
3085 .unwrap_or("RuleSet")
3086 .to_string(),
3087 topic_arn: s.get("TopicArn").and_then(|v| v.as_str()).map(String::from),
3088 });
3089 }
3090 None
3091}
3092
3093fn make_apigwv2_id(n: usize) -> String {
3097 let s = uuid::Uuid::new_v4().simple().to_string();
3098 s[..n.min(s.len())].to_string()
3099}
3100
3101fn cfn_as_i64(v: &serde_json::Value) -> Option<i64> {
3111 if let Some(n) = v.as_i64() {
3112 return Some(n);
3113 }
3114 v.as_str().and_then(|s| s.parse::<i64>().ok())
3115}
3116
3117fn lowercase_first_keys(value: serde_json::Value) -> serde_json::Value {
3118 match value {
3119 serde_json::Value::Object(map) => {
3120 let mut out = serde_json::Map::new();
3121 for (k, v) in map {
3122 let new_key = if let Some(first) = k.chars().next() {
3123 let mut s = String::with_capacity(k.len());
3124 s.extend(first.to_lowercase());
3125 s.push_str(&k[first.len_utf8()..]);
3126 s
3127 } else {
3128 k
3129 };
3130 out.insert(new_key, lowercase_first_keys(v));
3131 }
3132 serde_json::Value::Object(out)
3133 }
3134 serde_json::Value::Array(arr) => {
3135 serde_json::Value::Array(arr.into_iter().map(lowercase_first_keys).collect())
3136 }
3137 other => other,
3138 }
3139}
3140
3141fn synth_acm_domain_validation(
3148 domain_name: &str,
3149 sans: &[String],
3150 validation_method: &str,
3151) -> Vec<AcmDomainValidation> {
3152 let mut all = vec![domain_name.to_string()];
3153 for s in sans {
3154 if !all.contains(s) {
3155 all.push(s.clone());
3156 }
3157 }
3158 all.into_iter()
3159 .map(|name| AcmDomainValidation {
3160 domain_name: name.clone(),
3161 validation_status: "SUCCESS".to_string(),
3162 validation_method: validation_method.to_string(),
3163 resource_record_name: Some(format!("_amzn-validations.{name}.")),
3164 resource_record_type: Some("CNAME".to_string()),
3165 resource_record_value: Some(format!("{}.acm-validations.aws.", Uuid::new_v4())),
3166 })
3167 .collect()
3168}
3169
3170fn parse_acm_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
3172 let mut out = BTreeMap::new();
3173 if let Some(arr) = value.and_then(|v| v.as_array()) {
3174 for t in arr {
3175 if let (Some(k), Some(v)) = (
3176 t.get("Key").and_then(|v| v.as_str()),
3177 t.get("Value").and_then(|v| v.as_str()),
3178 ) {
3179 out.insert(k.to_string(), v.to_string());
3180 }
3181 }
3182 }
3183 out
3184}
3185
3186fn parse_ecs_tags(value: Option<&serde_json::Value>) -> Vec<EcsTagEntry> {
3188 let Some(arr) = value.and_then(|v| v.as_array()) else {
3189 return Vec::new();
3190 };
3191 arr.iter()
3192 .filter_map(|t| {
3193 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
3194 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
3195 Some(EcsTagEntry { key, value })
3196 })
3197 .collect()
3198}
3199
3200fn parse_ecs_cluster_name(input: &str) -> String {
3203 if let Some(after) = input.split(":cluster/").nth(1) {
3204 return after.to_string();
3205 }
3206 input.to_string()
3207}
3208
3209fn parse_td_arn(input: &str) -> (String, i32) {
3213 let suffix = input.rsplit('/').next().unwrap_or(input);
3214 if let Some((family, rev)) = suffix.split_once(':') {
3215 if let Ok(revision) = rev.parse::<i32>() {
3216 return (family.to_string(), revision);
3217 }
3218 }
3219 (input.to_string(), 1)
3220}
3221
3222fn parse_service_arn(input: &str) -> Option<(String, String)> {
3225 let after = input.split(":service/").nth(1)?;
3226 let mut parts = after.splitn(2, '/');
3227 let cluster = parts.next()?.to_string();
3228 let service = parts.next()?.to_string();
3229 Some((cluster, service))
3230}
3231
3232fn parse_rds_tags(value: Option<&serde_json::Value>) -> Vec<RdsTag> {
3234 let Some(arr) = value.and_then(|v| v.as_array()) else {
3235 return Vec::new();
3236 };
3237 arr.iter()
3238 .filter_map(|t| {
3239 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
3240 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
3241 Some(RdsTag { key, value })
3242 })
3243 .collect()
3244}
3245
3246fn rds_extras_mut<'a>(
3250 state: &'a mut fakecloud_rds::RdsState,
3251 category: &str,
3252) -> &'a mut BTreeMap<String, serde_json::Value> {
3253 state.extras.entry(category.to_string()).or_default()
3254}
3255
3256fn parse_cognito_string_array(value: Option<&serde_json::Value>) -> Vec<String> {
3260 value
3261 .and_then(|v| v.as_array())
3262 .map(|arr| {
3263 arr.iter()
3264 .filter_map(|v| v.as_str().map(|s| s.to_string()))
3265 .collect()
3266 })
3267 .unwrap_or_default()
3268}
3269
3270fn parse_cognito_password_policy(value: Option<&serde_json::Value>) -> PasswordPolicy {
3271 let Some(inner) = value
3272 .and_then(|v| v.get("PasswordPolicy"))
3273 .and_then(|v| v.as_object())
3274 else {
3275 return PasswordPolicy::default();
3276 };
3277 let mut p = PasswordPolicy::default();
3278 if let Some(n) = inner.get("MinimumLength").and_then(|v| v.as_i64()) {
3279 p.minimum_length = n;
3280 }
3281 if let Some(b) = inner.get("RequireUppercase").and_then(|v| v.as_bool()) {
3282 p.require_uppercase = b;
3283 }
3284 if let Some(b) = inner.get("RequireLowercase").and_then(|v| v.as_bool()) {
3285 p.require_lowercase = b;
3286 }
3287 if let Some(b) = inner.get("RequireNumbers").and_then(|v| v.as_bool()) {
3288 p.require_numbers = b;
3289 }
3290 if let Some(b) = inner.get("RequireSymbols").and_then(|v| v.as_bool()) {
3291 p.require_symbols = b;
3292 }
3293 if let Some(n) = inner
3294 .get("TemporaryPasswordValidityDays")
3295 .and_then(|v| v.as_i64())
3296 {
3297 p.temporary_password_validity_days = n;
3298 }
3299 p
3300}
3301
3302fn parse_cognito_schema_attribute(value: &serde_json::Value) -> Option<SchemaAttribute> {
3303 let name = value.get("Name").and_then(|v| v.as_str())?.to_string();
3304 Some(SchemaAttribute {
3305 name,
3306 attribute_data_type: value
3307 .get("AttributeDataType")
3308 .and_then(|v| v.as_str())
3309 .unwrap_or("String")
3310 .to_string(),
3311 developer_only_attribute: value
3312 .get("DeveloperOnlyAttribute")
3313 .and_then(|v| v.as_bool())
3314 .unwrap_or(false),
3315 mutable: value
3316 .get("Mutable")
3317 .and_then(|v| v.as_bool())
3318 .unwrap_or(true),
3319 required: value
3320 .get("Required")
3321 .and_then(|v| v.as_bool())
3322 .unwrap_or(false),
3323 string_attribute_constraints: None,
3324 number_attribute_constraints: None,
3325 })
3326}
3327
3328fn parse_cognito_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
3329 let mut out = BTreeMap::new();
3330 if let Some(obj) = value.and_then(|v| v.as_object()) {
3331 for (k, v) in obj {
3332 if let Some(s) = v.as_str() {
3333 out.insert(k.clone(), s.to_string());
3334 }
3335 }
3336 }
3337 out
3338}
3339
3340fn parse_cognito_email_configuration(
3341 value: Option<&serde_json::Value>,
3342) -> Option<EmailConfiguration> {
3343 let inner = value?.as_object()?;
3344 Some(EmailConfiguration {
3345 source_arn: inner
3346 .get("SourceArn")
3347 .and_then(|v| v.as_str())
3348 .map(|s| s.to_string()),
3349 reply_to_email_address: inner
3350 .get("ReplyToEmailAddress")
3351 .and_then(|v| v.as_str())
3352 .map(|s| s.to_string()),
3353 email_sending_account: inner
3354 .get("EmailSendingAccount")
3355 .and_then(|v| v.as_str())
3356 .map(|s| s.to_string()),
3357 from_email_address: inner
3358 .get("From")
3359 .and_then(|v| v.as_str())
3360 .map(|s| s.to_string()),
3361 configuration_set: inner
3362 .get("ConfigurationSet")
3363 .and_then(|v| v.as_str())
3364 .map(|s| s.to_string()),
3365 })
3366}
3367
3368fn parse_cognito_sms_configuration(value: Option<&serde_json::Value>) -> Option<SmsConfiguration> {
3369 let inner = value?.as_object()?;
3370 Some(SmsConfiguration {
3371 sns_caller_arn: inner
3372 .get("SnsCallerArn")
3373 .and_then(|v| v.as_str())
3374 .map(|s| s.to_string()),
3375 external_id: inner
3376 .get("ExternalId")
3377 .and_then(|v| v.as_str())
3378 .map(|s| s.to_string()),
3379 sns_region: inner
3380 .get("SnsRegion")
3381 .and_then(|v| v.as_str())
3382 .map(|s| s.to_string()),
3383 })
3384}
3385
3386fn parse_cognito_admin_create_user_config(
3387 value: Option<&serde_json::Value>,
3388) -> Option<AdminCreateUserConfig> {
3389 let inner = value?.as_object()?;
3390 Some(AdminCreateUserConfig {
3391 allow_admin_create_user_only: inner
3392 .get("AllowAdminCreateUserOnly")
3393 .and_then(|v| v.as_bool()),
3394 invite_message_template: None,
3395 unused_account_validity_days: inner
3396 .get("UnusedAccountValidityDays")
3397 .and_then(|v| v.as_i64()),
3398 })
3399}
3400
3401fn parse_cognito_account_recovery(
3402 value: Option<&serde_json::Value>,
3403) -> Option<AccountRecoverySetting> {
3404 let arr = value?.get("RecoveryMechanisms")?.as_array()?;
3405 Some(AccountRecoverySetting {
3406 recovery_mechanisms: arr
3407 .iter()
3408 .filter_map(|m| {
3409 let name = m.get("Name").and_then(|v| v.as_str())?.to_string();
3410 let priority = m.get("Priority").and_then(|v| v.as_i64()).unwrap_or(1);
3411 Some(RecoveryOption { name, priority })
3412 })
3413 .collect(),
3414 })
3415}
3416
3417fn parse_firehose_s3_destination(value: &serde_json::Value) -> Result<S3Destination, String> {
3418 let role_arn = value
3419 .get("RoleARN")
3420 .and_then(|v| v.as_str())
3421 .ok_or("S3 destination requires RoleARN")?
3422 .to_string();
3423 let bucket_arn = value
3424 .get("BucketARN")
3425 .and_then(|v| v.as_str())
3426 .ok_or("S3 destination requires BucketARN")?
3427 .to_string();
3428 let prefix = value
3429 .get("Prefix")
3430 .and_then(|v| v.as_str())
3431 .map(|s| s.to_string());
3432 let error_output_prefix = value
3433 .get("ErrorOutputPrefix")
3434 .and_then(|v| v.as_str())
3435 .map(|s| s.to_string());
3436 let mut buffering_size_mb = None;
3437 let mut buffering_interval_seconds = None;
3438 if let Some(hints) = value.get("BufferingHints") {
3439 buffering_size_mb = hints.get("SizeInMBs").and_then(|v| v.as_i64());
3440 buffering_interval_seconds = hints.get("IntervalInSeconds").and_then(|v| v.as_i64());
3441 }
3442 let compression_format = value
3443 .get("CompressionFormat")
3444 .and_then(|v| v.as_str())
3445 .map(|s| s.to_string());
3446
3447 Ok(S3Destination {
3448 destination_id: "destination-1".to_string(),
3449 role_arn,
3450 bucket_arn,
3451 prefix,
3452 error_output_prefix,
3453 buffering_size_mb,
3454 buffering_interval_seconds,
3455 compression_format,
3456 processing_configuration: None,
3457 data_format_conversion_configuration: None,
3458 cloudwatch_logging_options: None,
3459 custom_time_zone: None,
3460 s3_backup_mode: None,
3461 file_extension: None,
3462 })
3463}
3464
3465#[cfg(test)]
3466mod tests {
3467 use super::*;
3468 use parking_lot::RwLock;
3469
3470 fn make_provisioner() -> ResourceProvisioner {
3471 ResourceProvisioner {
3472 sqs_state: Arc::new(RwLock::new(
3473 fakecloud_core::multi_account::MultiAccountState::new(
3474 "123456789012",
3475 "us-east-1",
3476 "http://localhost:4566",
3477 ),
3478 )),
3479 sns_state: Arc::new(RwLock::new(
3480 fakecloud_core::multi_account::MultiAccountState::new(
3481 "123456789012",
3482 "us-east-1",
3483 "http://localhost:4566",
3484 ),
3485 )),
3486 ssm_state: Arc::new(RwLock::new(
3487 fakecloud_core::multi_account::MultiAccountState::new(
3488 "123456789012",
3489 "us-east-1",
3490 "http://localhost:4566",
3491 ),
3492 )),
3493 iam_state: Arc::new(RwLock::new(
3494 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "http://localhost:4566"),
3495 )),
3496 s3_state: Arc::new(RwLock::new(fakecloud_core::multi_account::MultiAccountState::new(
3497 "123456789012",
3498 "us-east-1", "",
3499 ))),
3500 eventbridge_state: Arc::new(RwLock::new(
3501 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3502 )),
3503 dynamodb_state: Arc::new(RwLock::new(fakecloud_core::multi_account::MultiAccountState::new(
3504 "123456789012",
3505 "us-east-1", "",
3506 ))),
3507 logs_state: Arc::new(RwLock::new(
3508 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3509 )),
3510 lambda_state: Arc::new(RwLock::new(
3511 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3512 )),
3513 secretsmanager_state: Arc::new(RwLock::new(
3514 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3515 )),
3516 kinesis_state: Arc::new(RwLock::new(
3517 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3518 )),
3519 kms_state: Arc::new(RwLock::new(
3520 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3521 )),
3522 ecr_state: Arc::new(RwLock::new(
3523 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3524 )),
3525 cloudwatch_state: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
3526 elbv2_state: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
3527 organizations_state: Arc::new(RwLock::new(None)),
3528 cognito_state: Arc::new(RwLock::new(
3529 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3530 )),
3531 rds_state: Arc::new(RwLock::new(
3532 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3533 )),
3534 ec2_state: Arc::new(RwLock::new(
3535 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3536 )),
3537 autoscaling_state: Arc::new(RwLock::new(
3538 fakecloud_autoscaling::AutoScalingAccounts::new(),
3539 )),
3540 batch_state: Arc::new(RwLock::new(fakecloud_batch::BatchAccounts::new())),
3541 pipes_state: Arc::new(RwLock::new(fakecloud_pipes::PipesAccounts::new())),
3542 ecs_state: Arc::new(RwLock::new(
3543 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3544 )),
3545 acm_state: Arc::new(RwLock::new(fakecloud_acm::AcmAccounts::new())),
3546 acmpca_state: Arc::new(RwLock::new(fakecloud_acmpca::AcmPcaAccounts::new())),
3547 config_state: Arc::new(RwLock::new(fakecloud_config::ConfigAccounts::new())),
3548 route53resolver_state: Arc::new(RwLock::new(
3549 fakecloud_route53resolver::Route53ResolverAccounts::new(),
3550 )),
3551 elasticache_state: Arc::new(RwLock::new(
3552 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3553 )),
3554 route53_state: Arc::new(RwLock::new(fakecloud_route53::Route53Accounts::new())),
3555 cloudfront_state: Arc::new(RwLock::new(
3556 fakecloud_cloudfront::CloudFrontAccounts::new(),
3557 )),
3558 cloudformation_state: Arc::new(RwLock::new(
3559 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3560 )),
3561 stepfunctions_state: Arc::new(RwLock::new(
3562 fakecloud_core::multi_account::MultiAccountState::new(
3563 "123456789012",
3564 "us-east-1",
3565 "",
3566 ),
3567 )),
3568 wafv2_state: Arc::new(RwLock::new(fakecloud_wafv2::Wafv2Accounts::default())),
3569 apigateway_state: Arc::new(RwLock::new(
3570 fakecloud_core::multi_account::MultiAccountState::new(
3571 "123456789012",
3572 "us-east-1",
3573 "",
3574 ),
3575 )),
3576 apigatewayv2_state: Arc::new(RwLock::new(
3577 fakecloud_core::multi_account::MultiAccountState::new(
3578 "123456789012",
3579 "us-east-1",
3580 "",
3581 ),
3582 )),
3583 ses_state: Arc::new(RwLock::new(
3584 fakecloud_core::multi_account::MultiAccountState::new(
3585 "123456789012",
3586 "us-east-1",
3587 "",
3588 ),
3589 )),
3590 app_autoscaling_state: Arc::new(parking_lot::RwLock::new(
3591 fakecloud_application_autoscaling::ApplicationAutoScalingAccounts::new(),
3592 )),
3593 athena_state: Arc::new(parking_lot::RwLock::new(
3594 fakecloud_athena::AthenaAccounts::new(),
3595 )),
3596 firehose_state: Arc::new(parking_lot::RwLock::new(
3597 fakecloud_firehose::FirehoseAccounts::new(),
3598 )),
3599 glue_state: Arc::new(parking_lot::RwLock::new(
3600 fakecloud_glue::GlueAccounts::new(),
3601 )),
3602 eks_state: Arc::new(parking_lot::RwLock::new(
3603 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3604 )),
3605 servicediscovery_state: Arc::new(parking_lot::RwLock::new(
3606 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3607 )),
3608 codeartifact_state: Arc::new(parking_lot::RwLock::new(
3609 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3610 )),
3611 codecommit_state: Arc::new(parking_lot::RwLock::new(
3612 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3613 )),
3614 efs_state: Arc::new(parking_lot::RwLock::new(
3615 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3616 )),
3617 elasticbeanstalk_state: Arc::new(parking_lot::RwLock::new(
3618 fakecloud_elasticbeanstalk::EbAccounts::new(),
3619 )),
3620 mq_state: Arc::new(parking_lot::RwLock::new(
3621 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3622 )),
3623 kafka_state: Arc::new(parking_lot::RwLock::new(
3624 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3625 )),
3626 ka2_state: Arc::new(parking_lot::RwLock::new(
3627 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3628 )),
3629 delivery: Arc::new(DeliveryBus::new()),
3630 lambda_runtime: None,
3631 rds_runtime: None,
3632 ec2_runtime: None,
3633 ecs_runtime: None,
3634 elasticache_runtime: None,
3635 mq_runtime: None,
3636 kafka_runtime: None,
3637 pending_container_spawns: Arc::new(parking_lot::Mutex::new(Vec::new())),
3638 pending_container_teardowns: Arc::new(parking_lot::Mutex::new(Vec::new())),
3639 pending_custom_invokes: Arc::new(parking_lot::Mutex::new(Vec::new())),
3640 defer_custom_invokes: false,
3641 s3_store: Arc::new(fakecloud_persistence::s3::MemoryS3Store::new()),
3642 account_id: "123456789012".to_string(),
3643 region: "us-east-1".to_string(),
3644 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test/00000000-0000-0000-0000-000000000000".to_string(),
3645 strict_unknown_types: false,
3646 }
3647 }
3648
3649 fn make_resource(
3650 resource_type: &str,
3651 logical_id: &str,
3652 props: serde_json::Value,
3653 ) -> ResourceDefinition {
3654 ResourceDefinition {
3655 logical_id: logical_id.to_string(),
3656 resource_type: resource_type.to_string(),
3657 properties: props,
3658 }
3659 }
3660
3661 #[test]
3662 fn cloudwatch_alarm_provisions_and_updates_metrics() {
3663 let prov = make_provisioner();
3664 let created = prov
3665 .create_resource(&make_resource(
3666 "AWS::CloudWatch::Alarm",
3667 "A",
3668 serde_json::json!({
3669 "AlarmName": "math-alarm",
3670 "ComparisonOperator": "GreaterThanUpperThreshold",
3671 "EvaluationPeriods": 1,
3672 "ThresholdMetricId": "ad1",
3673 "Metrics": [
3674 {"Id": "e1", "Expression": "m1", "Label": "expr", "ReturnData": true},
3675 {"Id": "m1", "ReturnData": false, "MetricStat": {
3676 "Metric": {
3677 "Namespace": "AWS/EC2",
3678 "MetricName": "CPUUtilization",
3679 "Dimensions": [{"Name": "InstanceId", "Value": "i-123"}]
3680 },
3681 "Period": 300,
3682 "Stat": "Average"
3683 }}
3684 ]
3685 }),
3686 ))
3687 .expect("alarm provisions");
3688
3689 {
3690 let cw = prov.cloudwatch_state.read();
3691 let acct = cw.get("123456789012").unwrap();
3692 let alarm = acct
3693 .alarms_in("us-east-1")
3694 .unwrap()
3695 .get("math-alarm")
3696 .unwrap();
3697 assert_eq!(alarm.metrics.len(), 2, "Metrics parsed on create");
3698 assert_eq!(alarm.metrics[0].id, "e1");
3699 assert_eq!(alarm.metrics[0].expression.as_deref(), Some("m1"));
3700 assert_eq!(alarm.metrics[0].return_data, Some(true));
3701 assert_eq!(alarm.threshold_metric_id.as_deref(), Some("ad1"));
3702 let stat = alarm.metrics[1].metric_stat.as_ref().unwrap();
3703 assert_eq!(stat.metric_name.as_deref(), Some("CPUUtilization"));
3704 assert_eq!(
3705 stat.dimensions.get("InstanceId").map(String::as_str),
3706 Some("i-123")
3707 );
3708 assert_eq!(stat.stat.as_deref(), Some("Average"));
3709 assert_eq!(stat.period, Some(300));
3710 }
3711
3712 prov.update_resource(
3714 &created,
3715 &make_resource(
3716 "AWS::CloudWatch::Alarm",
3717 "A",
3718 serde_json::json!({
3719 "AlarmName": "math-alarm",
3720 "ComparisonOperator": "GreaterThanUpperThreshold",
3721 "EvaluationPeriods": 1,
3722 "ThresholdMetricId": "ad2",
3723 "Metrics": [{"Id": "e2", "Expression": "m1*2", "ReturnData": true}]
3724 }),
3725 ),
3726 )
3727 .expect("update succeeds")
3728 .expect("AWS::CloudWatch::Alarm is updatable");
3729
3730 let cw = prov.cloudwatch_state.read();
3731 let acct = cw.get("123456789012").unwrap();
3732 let alarm = acct
3733 .alarms_in("us-east-1")
3734 .unwrap()
3735 .get("math-alarm")
3736 .unwrap();
3737 assert_eq!(alarm.metrics.len(), 1, "Metrics re-parsed on update");
3738 assert_eq!(alarm.metrics[0].id, "e2");
3739 assert_eq!(alarm.metrics[0].expression.as_deref(), Some("m1*2"));
3740 assert_eq!(
3742 alarm.threshold_metric_id.as_deref(),
3743 Some("ad2"),
3744 "ThresholdMetricId refreshed on update"
3745 );
3746 }
3747
3748 #[test]
3749 fn update_stack_reconciles_iam_role_policies() {
3750 let prov = make_provisioner();
3751 let created = prov
3752 .create_resource(&make_resource(
3753 "AWS::IAM::Role",
3754 "R",
3755 serde_json::json!({
3756 "RoleName": "r1",
3757 "AssumeRolePolicyDocument": {"Version": "2012-10-17"},
3758 "ManagedPolicyArns": ["arn:aws:iam::aws:policy/ReadOnlyAccess"],
3759 }),
3760 ))
3761 .expect("role provisions");
3762 prov.update_resource(
3763 &created,
3764 &make_resource(
3765 "AWS::IAM::Role",
3766 "R",
3767 serde_json::json!({
3768 "RoleName": "r1",
3769 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []},
3770 "Description": "updated",
3771 "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AdministratorAccess"],
3772 "Policies": [{"PolicyName": "inline1", "PolicyDocument": {"k": "v"}}],
3773 }),
3774 ),
3775 )
3776 .expect("update succeeds")
3777 .expect("IAM::Role is updatable");
3778 let iam = prov.iam_state.read();
3779 let acct = iam.get("123456789012").unwrap();
3780 let role = acct.roles.get("r1").unwrap();
3781 assert_eq!(role.description.as_deref(), Some("updated"));
3782 assert!(role.assume_role_policy_document.contains("Statement"));
3783 let attached = acct.role_policies.get("r1").unwrap();
3785 assert_eq!(
3786 attached,
3787 &vec!["arn:aws:iam::aws:policy/AdministratorAccess".to_string()]
3788 );
3789 assert!(acct
3790 .role_inline_policies
3791 .get("r1")
3792 .unwrap()
3793 .contains_key("inline1"));
3794 }
3795
3796 #[test]
3797 fn update_stack_bumps_managed_policy_version() {
3798 let prov = make_provisioner();
3799 let created = prov
3800 .create_resource(&make_resource(
3801 "AWS::IAM::ManagedPolicy",
3802 "P",
3803 serde_json::json!({
3804 "ManagedPolicyName": "p1",
3805 "PolicyDocument": {"Version": "2012-10-17", "Statement": [{"Effect": "Allow"}]},
3806 }),
3807 ))
3808 .expect("managed policy provisions");
3809 prov.update_resource(
3810 &created,
3811 &make_resource(
3812 "AWS::IAM::ManagedPolicy",
3813 "P",
3814 serde_json::json!({
3815 "ManagedPolicyName": "p1",
3816 "PolicyDocument": {"Version": "2012-10-17", "Statement": [{"Effect": "Deny"}]},
3817 }),
3818 ),
3819 )
3820 .expect("update succeeds")
3821 .expect("IAM::ManagedPolicy is updatable");
3822 let iam = prov.iam_state.read();
3823 let acct = iam.get("123456789012").unwrap();
3824 let policy = acct.policies.get(&created.physical_id).unwrap();
3825 assert_eq!(policy.versions.len(), 2);
3826 assert_eq!(policy.default_version_id, "v2");
3827 let default = policy.versions.iter().find(|v| v.is_default).unwrap();
3828 assert!(default.document.contains("Deny"));
3829 }
3830
3831 fn pipe_props(name: &str, target: &str, description: Option<&str>) -> serde_json::Value {
3832 let mut m = serde_json::json!({
3833 "Name": name,
3834 "Source": "arn:aws:sqs:us-east-1:123456789012:src",
3835 "Target": target,
3836 "RoleArn": "arn:aws:iam::123456789012:role/pipe",
3837 });
3838 if let Some(d) = description {
3839 m["Description"] = serde_json::json!(d);
3840 }
3841 m
3842 }
3843
3844 #[test]
3845 fn update_stack_applies_pipes_pipe_change() {
3846 let prov = make_provisioner();
3847 let created = prov
3848 .create_resource(&make_resource(
3849 "AWS::Pipes::Pipe",
3850 "P",
3851 pipe_props(
3852 "my-pipe",
3853 "arn:aws:sqs:us-east-1:123456789012:dst",
3854 Some("v1"),
3855 ),
3856 ))
3857 .expect("pipe provisions");
3858
3859 let updated = prov
3862 .update_resource(
3863 &created,
3864 &make_resource(
3865 "AWS::Pipes::Pipe",
3866 "P",
3867 pipe_props(
3868 "my-pipe",
3869 "arn:aws:sqs:us-east-1:123456789012:dst2",
3870 Some("v2"),
3871 ),
3872 ),
3873 )
3874 .expect("update succeeds")
3875 .expect("Pipes::Pipe is updatable (not a silent no-op)");
3876 assert_eq!(updated.status, "UPDATE_COMPLETE");
3877
3878 let pipes = prov.pipes_state.read();
3880 let pipe = pipes
3881 .get("123456789012")
3882 .unwrap()
3883 .pipes
3884 .get("my-pipe")
3885 .unwrap();
3886 assert_eq!(pipe["Description"], "v2");
3887 assert_eq!(pipe["Target"], "arn:aws:sqs:us-east-1:123456789012:dst2");
3888 assert_eq!(pipe["CurrentState"], "RUNNING");
3890 }
3891
3892 #[test]
3893 fn update_stack_replaces_pipe_on_source_change() {
3894 let prov = make_provisioner();
3898 let created = prov
3899 .create_resource(&make_resource(
3900 "AWS::Pipes::Pipe",
3901 "P",
3902 serde_json::json!({
3903 "Name": "src-pipe",
3904 "Source": "arn:aws:sqs:us-east-1:123456789012:src-old",
3905 "Target": "arn:aws:sqs:us-east-1:123456789012:dst",
3906 "RoleArn": "arn:aws:iam::123456789012:role/pipe",
3907 }),
3908 ))
3909 .expect("pipe provisions");
3910 prov.update_resource(
3911 &created,
3912 &make_resource(
3913 "AWS::Pipes::Pipe",
3914 "P",
3915 serde_json::json!({
3916 "Name": "src-pipe",
3917 "Source": "arn:aws:sqs:us-east-1:123456789012:src-new",
3918 "Target": "arn:aws:sqs:us-east-1:123456789012:dst",
3919 "RoleArn": "arn:aws:iam::123456789012:role/pipe",
3920 }),
3921 ),
3922 )
3923 .expect("update succeeds")
3924 .expect("Pipes::Pipe is updatable");
3925 let pipes = prov.pipes_state.read();
3926 let acct = pipes.get("123456789012").unwrap();
3927 assert_eq!(acct.pipes.len(), 1);
3929 let pipe = acct.pipes.get("src-pipe").unwrap();
3930 assert_eq!(
3931 pipe["Source"], "arn:aws:sqs:us-east-1:123456789012:src-new",
3932 "changed Source is applied via replacement, not silently dropped"
3933 );
3934 }
3935
3936 #[test]
3937 fn update_stack_clears_omitted_pipe_field() {
3938 let prov = make_provisioner();
3939 let created = prov
3940 .create_resource(&make_resource(
3941 "AWS::Pipes::Pipe",
3942 "P",
3943 pipe_props(
3944 "p2",
3945 "arn:aws:sqs:us-east-1:123456789012:dst",
3946 Some("drop-me"),
3947 ),
3948 ))
3949 .expect("pipe provisions");
3950 prov.update_resource(
3951 &created,
3952 &make_resource(
3953 "AWS::Pipes::Pipe",
3954 "P",
3955 pipe_props("p2", "arn:aws:sqs:us-east-1:123456789012:dst", None),
3956 ),
3957 )
3958 .expect("update succeeds")
3959 .expect("updatable");
3960 let pipes = prov.pipes_state.read();
3961 let pipe = pipes.get("123456789012").unwrap().pipes.get("p2").unwrap();
3962 assert!(
3963 pipe.get("Description").is_none(),
3964 "an omitted updatable field is cleared (full-replace semantics)"
3965 );
3966 }
3967
3968 #[test]
3969 fn create_pipe_rejects_empty_required_field() {
3970 let prov = make_provisioner();
3971 let err = prov
3972 .create_resource(&make_resource(
3973 "AWS::Pipes::Pipe",
3974 "P",
3975 serde_json::json!({
3976 "Name": "p3",
3977 "Source": "",
3978 "Target": "arn:aws:sqs:us-east-1:123456789012:dst",
3979 "RoleArn": "arn:aws:iam::123456789012:role/pipe",
3980 }),
3981 ))
3982 .unwrap_err();
3983 assert!(err.contains("Source"), "empty Source rejected: {err}");
3984 }
3985
3986 #[test]
3987 fn create_pipe_rejects_duplicate_name() {
3988 let prov = make_provisioner();
3989 let props = pipe_props("dup", "arn:aws:sqs:us-east-1:123456789012:dst", None);
3990 prov.create_resource(&make_resource("AWS::Pipes::Pipe", "P", props.clone()))
3991 .expect("first create ok");
3992 let err = prov
3994 .create_resource(&make_resource("AWS::Pipes::Pipe", "P2", props))
3995 .unwrap_err();
3996 assert!(err.contains("already exists"), "duplicate rejected: {err}");
3997 }
3998
3999 #[test]
4000 fn update_stack_applies_sqs_queue_property_change() {
4001 let prov = make_provisioner();
4002 let created = prov
4003 .create_resource(&make_resource(
4004 "AWS::SQS::Queue",
4005 "Q",
4006 serde_json::json!({ "QueueName": "q1", "VisibilityTimeout": "30" }),
4007 ))
4008 .expect("queue provisions");
4009 let updated = prov
4011 .update_resource(
4012 &created,
4013 &make_resource(
4014 "AWS::SQS::Queue",
4015 "Q",
4016 serde_json::json!({ "QueueName": "q1", "VisibilityTimeout": "120" }),
4017 ),
4018 )
4019 .expect("update succeeds")
4020 .expect("SQS::Queue is an updatable type");
4021 assert_eq!(updated.physical_id, created.physical_id);
4022 let sqs = prov.sqs_state.read();
4023 let acct = sqs.get("123456789012").unwrap();
4024 let queue = acct.queues.get(&created.physical_id).unwrap();
4025 assert_eq!(
4026 queue
4027 .attributes
4028 .get("VisibilityTimeout")
4029 .map(String::as_str),
4030 Some("120")
4031 );
4032 }
4033
4034 #[test]
4035 fn update_stack_applies_sns_topic_property_change() {
4036 let prov = make_provisioner();
4037 let created = prov
4038 .create_resource(&make_resource(
4039 "AWS::SNS::Topic",
4040 "T",
4041 serde_json::json!({ "TopicName": "t1", "DisplayName": "before" }),
4042 ))
4043 .expect("topic provisions");
4044 let updated = prov
4045 .update_resource(
4046 &created,
4047 &make_resource(
4048 "AWS::SNS::Topic",
4049 "T",
4050 serde_json::json!({ "TopicName": "t1", "DisplayName": "after" }),
4051 ),
4052 )
4053 .expect("update succeeds")
4054 .expect("SNS::Topic is an updatable type");
4055 assert_eq!(updated.physical_id, created.physical_id);
4056 let sns = prov.sns_state.read();
4057 let acct = sns.get("123456789012").unwrap();
4058 let topic = acct.topics.get(&created.physical_id).unwrap();
4059 assert_eq!(
4060 topic.attributes.get("DisplayName").map(String::as_str),
4061 Some("after")
4062 );
4063 }
4064
4065 #[test]
4066 fn ec2_vpc_subnet_provision_through_real_handlers() {
4067 let prov = make_provisioner();
4068 let vpc = prov
4069 .create_resource(&make_resource(
4070 "AWS::EC2::VPC",
4071 "Vpc",
4072 serde_json::json!({ "CidrBlock": "10.1.0.0/16" }),
4073 ))
4074 .expect("VPC provisions");
4075 assert!(
4076 vpc.physical_id.starts_with("vpc-"),
4077 "got {}",
4078 vpc.physical_id
4079 );
4080 {
4083 let ec2 = prov.ec2_state.read();
4084 let acct = ec2.get("123456789012").unwrap();
4085 assert!(acct.vpcs.contains_key(&vpc.physical_id));
4086 }
4087 assert_eq!(
4089 prov.get_att(&vpc, "VpcId").as_deref(),
4090 Some(vpc.physical_id.as_str())
4091 );
4092 assert_eq!(
4093 prov.get_att(&vpc, "CidrBlock").as_deref(),
4094 Some("10.1.0.0/16")
4095 );
4096
4097 let subnet = prov
4099 .create_resource(&make_resource(
4100 "AWS::EC2::Subnet",
4101 "Subnet",
4102 serde_json::json!({ "VpcId": vpc.physical_id, "CidrBlock": "10.1.1.0/24" }),
4103 ))
4104 .expect("subnet provisions");
4105 assert!(subnet.physical_id.starts_with("subnet-"));
4106 {
4107 let ec2 = prov.ec2_state.read();
4108 let acct = ec2.get("123456789012").unwrap();
4109 assert!(acct.subnets.contains_key(&subnet.physical_id));
4110 }
4111
4112 prov.delete_resource(&subnet).expect("subnet deletes");
4114 prov.delete_resource(&vpc).expect("vpc deletes");
4115 }
4116
4117 #[test]
4118 fn unknown_resource_type_records_instead_of_failing() {
4119 let prov = make_provisioner();
4120 let sr = prov
4124 .create_resource(&make_resource(
4125 "AWS::CloudFormation::WaitConditionHandle",
4126 "Handle",
4127 serde_json::json!({}),
4128 ))
4129 .expect("unknown resource type should record, not fail");
4130 assert_eq!(sr.physical_id, "Handle");
4131 assert_eq!(sr.status, "CREATE_COMPLETE");
4132 prov.delete_resource(&sr)
4134 .expect("delete no-op should succeed");
4135 }
4136
4137 #[test]
4138 fn ecr_repository_uri_uses_bound_endpoint_not_public_dns() {
4139 let prov = make_provisioner();
4143 let sr = prov
4144 .create_resource(&make_resource(
4145 "AWS::ECR::Repository",
4146 "Repo",
4147 serde_json::json!({ "RepositoryName": "my-repo" }),
4148 ))
4149 .expect("ECR repo provisions");
4150 let uri = sr
4151 .attributes
4152 .get("RepositoryUri")
4153 .expect("RepositoryUri attribute");
4154 assert!(
4155 !uri.contains("amazonaws.com"),
4156 "RepositoryUri must not use the public ECR DNS, got {uri}"
4157 );
4158 assert!(uri.contains("my-repo"), "uri should name the repo: {uri}");
4159 }
4160
4161 #[test]
4162 fn sns_subscription_rejects_nonexistent_topic() {
4163 let prov = make_provisioner();
4164 let resource = make_resource(
4165 "AWS::SNS::Subscription",
4166 "MySub",
4167 serde_json::json!({
4168 "TopicArn": "arn:aws:sns:us-east-1:123456789012:NonExistent",
4169 "Protocol": "sqs",
4170 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:my-queue"
4171 }),
4172 );
4173 let result = prov.create_resource(&resource);
4174 assert!(result.is_err());
4175 assert!(result.unwrap_err().contains("does not exist"));
4176 }
4177
4178 #[test]
4179 fn sns_subscription_succeeds_when_topic_exists() {
4180 let prov = make_provisioner();
4181 let topic = make_resource(
4183 "AWS::SNS::Topic",
4184 "MyTopic",
4185 serde_json::json!({ "TopicName": "my-topic" }),
4186 );
4187 let topic_result = prov.create_resource(&topic);
4188 assert!(topic_result.is_ok());
4189 let topic_arn = topic_result.unwrap().physical_id;
4190
4191 let sub = make_resource(
4193 "AWS::SNS::Subscription",
4194 "MySub",
4195 serde_json::json!({
4196 "TopicArn": topic_arn,
4197 "Protocol": "sqs",
4198 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:my-queue"
4199 }),
4200 );
4201 let result = prov.create_resource(&sub);
4202 assert!(result.is_ok());
4203 }
4204
4205 #[test]
4206 fn eventbridge_rule_arn_default_bus_omits_bus_name() {
4207 let prov = make_provisioner();
4208 let resource = make_resource(
4209 "AWS::Events::Rule",
4210 "MyRule",
4211 serde_json::json!({
4212 "Name": "my-rule",
4213 "ScheduleExpression": "rate(1 hour)"
4214 }),
4215 );
4216 let result = prov.create_resource(&resource).unwrap();
4217 assert_eq!(
4219 result.physical_id,
4220 "arn:aws:events:us-east-1:123456789012:rule/my-rule"
4221 );
4222 assert!(!result.physical_id.contains("rule/default/"));
4223 }
4224
4225 #[test]
4226 fn eventbridge_rule_arn_custom_bus_includes_bus_name() {
4227 let prov = make_provisioner();
4228 {
4230 let mut eb_accounts = prov.eventbridge_state.write();
4231 let state = eb_accounts.default_mut();
4232 state.buses.insert(
4233 "custom-bus".to_string(),
4234 fakecloud_eventbridge::EventBus {
4235 name: "custom-bus".to_string(),
4236 arn: "arn:aws:events:us-east-1:123456789012:event-bus/custom-bus".to_string(),
4237 policy: None,
4238 creation_time: Utc::now(),
4239 last_modified_time: Utc::now(),
4240 description: None,
4241 kms_key_identifier: None,
4242 dead_letter_config: None,
4243 tags: std::collections::BTreeMap::new(),
4244 },
4245 );
4246 }
4247 let resource = make_resource(
4248 "AWS::Events::Rule",
4249 "MyRule",
4250 serde_json::json!({
4251 "Name": "my-rule",
4252 "EventBusName": "custom-bus",
4253 "ScheduleExpression": "rate(1 hour)"
4254 }),
4255 );
4256 let result = prov.create_resource(&resource).unwrap();
4257 assert_eq!(
4258 result.physical_id,
4259 "arn:aws:events:us-east-1:123456789012:rule/custom-bus/my-rule"
4260 );
4261 }
4262
4263 #[test]
4264 fn eventbridge_rule_rejects_nonexistent_bus() {
4265 let prov = make_provisioner();
4266 let resource = make_resource(
4267 "AWS::Events::Rule",
4268 "MyRule",
4269 serde_json::json!({
4270 "Name": "my-rule",
4271 "EventBusName": "nonexistent-bus",
4272 "ScheduleExpression": "rate(1 hour)"
4273 }),
4274 );
4275 let result = prov.create_resource(&resource);
4276 assert!(result.is_err());
4277 assert!(result.unwrap_err().contains("does not exist"));
4278 }
4279
4280 #[test]
4281 fn custom_resource_requires_service_token() {
4282 let prov = make_provisioner();
4283 let resource = make_resource(
4284 "Custom::MyResource",
4285 "MyCustom",
4286 serde_json::json!({
4287 "Foo": "bar"
4288 }),
4289 );
4290 let result = prov.create_resource(&resource);
4291 assert!(result.is_err());
4292 assert!(
4293 result.unwrap_err().contains("ServiceToken"),
4294 "Should require ServiceToken property"
4295 );
4296 }
4297
4298 #[test]
4299 fn custom_resource_succeeds_without_lambda_delivery() {
4300 let prov = make_provisioner();
4303 let resource = make_resource(
4304 "Custom::MyResource",
4305 "MyCustom",
4306 serde_json::json!({
4307 "ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:my-func",
4308 "Foo": "bar"
4309 }),
4310 );
4311 let result = prov.create_resource(&resource);
4312 assert!(result.is_ok());
4313 let sr = result.unwrap();
4314 assert_eq!(sr.logical_id, "MyCustom");
4315 assert_eq!(sr.resource_type, "Custom::MyResource");
4316 assert!(sr.physical_id.starts_with("MyCustom-"));
4317 }
4318
4319 #[test]
4320 fn cloudformation_custom_resource_type_succeeds() {
4321 let prov = make_provisioner();
4322 let resource = make_resource(
4323 "AWS::CloudFormation::CustomResource",
4324 "MyCustom2",
4325 serde_json::json!({
4326 "ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:my-func",
4327 "Key": "value"
4328 }),
4329 );
4330 let result = prov.create_resource(&resource);
4331 assert!(result.is_ok());
4332 let sr = result.unwrap();
4333 assert_eq!(sr.resource_type, "AWS::CloudFormation::CustomResource");
4334 }
4335
4336 #[test]
4339 fn sqs_queue_create_and_delete() {
4340 let prov = make_provisioner();
4341 let res = make_resource(
4342 "AWS::SQS::Queue",
4343 "MyQ",
4344 serde_json::json!({"QueueName": "my-q"}),
4345 );
4346 let sr = prov.create_resource(&res).unwrap();
4347 assert!(sr.physical_id.contains("my-q"));
4348 assert_eq!(sr.resource_type, "AWS::SQS::Queue");
4349 prov.delete_resource(&sr).unwrap();
4350 }
4351
4352 #[test]
4353 fn sqs_queue_fifo_with_suffix() {
4354 let prov = make_provisioner();
4355 let res = make_resource(
4356 "AWS::SQS::Queue",
4357 "FifoQ",
4358 serde_json::json!({"QueueName": "my-fifo.fifo", "FifoQueue": true}),
4359 );
4360 let sr = prov.create_resource(&res).unwrap();
4361 assert!(sr.physical_id.contains(".fifo"));
4362 }
4363
4364 #[test]
4365 fn sns_topic_create_and_delete() {
4366 let prov = make_provisioner();
4367 let res = make_resource(
4368 "AWS::SNS::Topic",
4369 "MyTopic",
4370 serde_json::json!({"TopicName": "t1"}),
4371 );
4372 let sr = prov.create_resource(&res).unwrap();
4373 assert!(sr.physical_id.contains("t1"));
4374 prov.delete_resource(&sr).unwrap();
4375 }
4376
4377 #[test]
4378 fn ssm_parameter_create_and_delete() {
4379 let prov = make_provisioner();
4380 let res = make_resource(
4381 "AWS::SSM::Parameter",
4382 "MyParam",
4383 serde_json::json!({
4384 "Name": "/my/param",
4385 "Type": "String",
4386 "Value": "v1"
4387 }),
4388 );
4389 let sr = prov.create_resource(&res).unwrap();
4390 assert_eq!(sr.physical_id, "/my/param");
4391 prov.delete_resource(&sr).unwrap();
4392 }
4393
4394 #[test]
4395 fn iam_role_create_and_delete() {
4396 let prov = make_provisioner();
4397 let res = make_resource(
4398 "AWS::IAM::Role",
4399 "MyRole",
4400 serde_json::json!({
4401 "RoleName": "my-role",
4402 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
4403 }),
4404 );
4405 let sr = prov.create_resource(&res).unwrap();
4406 assert!(sr.physical_id.contains("my-role"));
4407 prov.delete_resource(&sr).unwrap();
4408 }
4409
4410 #[test]
4411 fn iam_policy_create_and_delete() {
4412 let prov = make_provisioner();
4413 let res = make_resource(
4414 "AWS::IAM::Policy",
4415 "MyPolicy",
4416 serde_json::json!({
4417 "PolicyName": "my-policy",
4418 "PolicyDocument": {"Version": "2012-10-17", "Statement": []}
4419 }),
4420 );
4421 let sr = prov.create_resource(&res).unwrap();
4422 assert!(sr.physical_id.contains("my-policy"));
4423 prov.delete_resource(&sr).unwrap();
4424 }
4425
4426 #[test]
4427 fn s3_bucket_create_and_delete() {
4428 let prov = make_provisioner();
4429 let res = make_resource(
4430 "AWS::S3::Bucket",
4431 "MyBucket",
4432 serde_json::json!({"BucketName": "my-bucket"}),
4433 );
4434 let sr = prov.create_resource(&res).unwrap();
4435 assert_eq!(sr.physical_id, "my-bucket");
4436 prov.delete_resource(&sr).unwrap();
4437 }
4438
4439 #[test]
4440 fn sqs_queue_policy_stored_on_queue_and_cleared_on_delete() {
4441 let prov = make_provisioner();
4442 let queue = prov
4443 .create_resource(&make_resource(
4444 "AWS::SQS::Queue",
4445 "Q",
4446 serde_json::json!({"QueueName": "q1"}),
4447 ))
4448 .unwrap();
4449 let policy = make_resource(
4451 "AWS::SQS::QueuePolicy",
4452 "QP",
4453 serde_json::json!({
4454 "Queues": [queue.physical_id.clone()],
4455 "PolicyDocument": {"Version": "2012-10-17", "Statement": [{
4456 "Effect": "Allow",
4457 "Principal": {"Service": "sns.amazonaws.com"},
4458 "Action": "sqs:SendMessage",
4459 "Resource": "*"
4460 }]}
4461 }),
4462 );
4463 let sr = prov.create_resource(&policy).unwrap();
4464
4465 {
4466 let mut accounts = prov.sqs_state.write();
4467 let state = accounts.get_or_create(&prov.account_id);
4468 let stored = state.queues[&queue.physical_id]
4469 .attributes
4470 .get("Policy")
4471 .expect("policy stored on queue");
4472 assert!(stored.contains("sqs:SendMessage"));
4473 }
4474
4475 prov.delete_resource(&sr).unwrap();
4476 {
4477 let mut accounts = prov.sqs_state.write();
4478 let state = accounts.get_or_create(&prov.account_id);
4479 assert!(!state.queues[&queue.physical_id]
4480 .attributes
4481 .contains_key("Policy"));
4482 }
4483 }
4484
4485 #[test]
4486 fn sns_topic_policy_stored_on_topic_and_cleared_on_delete() {
4487 let prov = make_provisioner();
4488 let topic = prov
4489 .create_resource(&make_resource(
4490 "AWS::SNS::Topic",
4491 "T",
4492 serde_json::json!({"TopicName": "t1"}),
4493 ))
4494 .unwrap();
4495 let policy = make_resource(
4496 "AWS::SNS::TopicPolicy",
4497 "TP",
4498 serde_json::json!({
4499 "Topics": [topic.physical_id.clone()],
4500 "PolicyDocument": {"Version": "2012-10-17", "Statement": [{
4501 "Effect": "Allow",
4502 "Principal": {"Service": "events.amazonaws.com"},
4503 "Action": "sns:Publish",
4504 "Resource": "*"
4505 }]}
4506 }),
4507 );
4508 let sr = prov.create_resource(&policy).unwrap();
4509
4510 {
4511 let mut accounts = prov.sns_state.write();
4512 let state = accounts.get_or_create(&prov.account_id);
4513 let stored = state.topics[&topic.physical_id]
4514 .attributes
4515 .get("Policy")
4516 .expect("policy stored on topic");
4517 assert!(stored.contains("sns:Publish"));
4518 }
4519
4520 prov.delete_resource(&sr).unwrap();
4521 {
4522 let mut accounts = prov.sns_state.write();
4523 let state = accounts.get_or_create(&prov.account_id);
4524 assert!(!state.topics[&topic.physical_id]
4525 .attributes
4526 .contains_key("Policy"));
4527 }
4528 }
4529
4530 #[test]
4531 fn s3_bucket_policy_stored_on_bucket_and_cleared_on_delete() {
4532 let prov = make_provisioner();
4533 let bucket = prov
4534 .create_resource(&make_resource(
4535 "AWS::S3::Bucket",
4536 "B",
4537 serde_json::json!({"BucketName": "b1"}),
4538 ))
4539 .unwrap();
4540 let policy = make_resource(
4541 "AWS::S3::BucketPolicy",
4542 "BP",
4543 serde_json::json!({
4544 "Bucket": bucket.physical_id.clone(),
4545 "PolicyDocument": {"Version": "2012-10-17", "Statement": [{
4546 "Effect": "Allow",
4547 "Principal": "*",
4548 "Action": "s3:GetObject",
4549 "Resource": "arn:aws:s3:::b1/*"
4550 }]}
4551 }),
4552 );
4553 let sr = prov.create_resource(&policy).unwrap();
4554 assert_eq!(sr.physical_id, "b1-policy");
4555
4556 {
4557 let mut accounts = prov.s3_state.write();
4558 let state = accounts.get_or_create(&prov.account_id);
4559 let stored = state.buckets[&bucket.physical_id]
4560 .policy
4561 .as_ref()
4562 .expect("policy stored on bucket");
4563 assert!(stored.contains("s3:GetObject"));
4564 }
4565
4566 prov.delete_resource(&sr).unwrap();
4567 {
4568 let mut accounts = prov.s3_state.write();
4569 let state = accounts.get_or_create(&prov.account_id);
4570 assert!(state.buckets[&bucket.physical_id].policy.is_none());
4571 }
4572 }
4573
4574 #[test]
4575 fn dynamodb_table_create_and_delete() {
4576 let prov = make_provisioner();
4577 let res = make_resource(
4578 "AWS::DynamoDB::Table",
4579 "MyTable",
4580 serde_json::json!({
4581 "TableName": "my-table",
4582 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
4583 "AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
4584 "BillingMode": "PAY_PER_REQUEST"
4585 }),
4586 );
4587 let sr = prov.create_resource(&res).unwrap();
4588 assert!(sr.physical_id.contains("my-table"));
4589 prov.delete_resource(&sr).unwrap();
4590 }
4591
4592 #[test]
4593 fn log_group_create_and_delete() {
4594 let prov = make_provisioner();
4595 let res = make_resource(
4596 "AWS::Logs::LogGroup",
4597 "MyLogs",
4598 serde_json::json!({"LogGroupName": "/app/logs"}),
4599 );
4600 let sr = prov.create_resource(&res).unwrap();
4601 assert!(sr.physical_id.contains("/app/logs"));
4602 prov.delete_resource(&sr).unwrap();
4603 }
4604
4605 #[test]
4606 fn lambda_function_create_and_delete() {
4607 let prov = make_provisioner();
4608 let res = make_resource(
4609 "AWS::Lambda::Function",
4610 "MyFn",
4611 serde_json::json!({
4612 "FunctionName": "my-fn",
4613 "Runtime": "nodejs20.x",
4614 "Role": "arn:aws:iam::123456789012:role/lambda-role",
4615 "Handler": "index.handler",
4616 "MemorySize": 256,
4617 "Timeout": 10,
4618 "Environment": {"Variables": {"FOO": "bar"}}
4619 }),
4620 );
4621 let sr = prov.create_resource(&res).unwrap();
4622 assert_eq!(sr.physical_id, "my-fn");
4623 assert_eq!(
4624 sr.attributes.get("Arn").map(String::as_str),
4625 Some("arn:aws:lambda:us-east-1:123456789012:function:my-fn")
4626 );
4627 {
4629 let lam = prov.lambda_state.read();
4630 let st = lam.get("123456789012").unwrap();
4631 let f = st.functions.get("my-fn").unwrap();
4632 assert_eq!(f.runtime, "nodejs20.x");
4633 assert_eq!(f.memory_size, 256);
4634 assert_eq!(f.environment.get("FOO").unwrap(), "bar");
4635 }
4636 prov.delete_resource(&sr).unwrap();
4637 let lam = prov.lambda_state.read();
4638 let st = lam.get("123456789012").unwrap();
4639 assert!(!st.functions.contains_key("my-fn"));
4640 }
4641
4642 #[test]
4643 fn unsupported_resource_type_is_recorded_not_failed() {
4644 let prov = make_provisioner();
4648 let res = make_resource("AWS::NonExistent::Thing", "X", serde_json::json!({}));
4649 let sr = prov.create_resource(&res).unwrap();
4650 assert_eq!(sr.physical_id, "X");
4651 }
4652
4653 #[test]
4654 fn iam_role_with_inline_policies() {
4655 let prov = make_provisioner();
4656 let res = make_resource(
4657 "AWS::IAM::Role",
4658 "MyRole",
4659 serde_json::json!({
4660 "RoleName": "role-inline",
4661 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []},
4662 "Policies": [
4663 {
4664 "PolicyName": "inline-1",
4665 "PolicyDocument": {"Version": "2012-10-17", "Statement": []}
4666 }
4667 ]
4668 }),
4669 );
4670 let sr = prov.create_resource(&res).unwrap();
4671 assert!(sr.physical_id.contains("role-inline"));
4672 }
4673
4674 #[test]
4675 fn sqs_queue_auto_name() {
4676 let prov = make_provisioner();
4677 let res = make_resource("AWS::SQS::Queue", "AutoQ", serde_json::json!({}));
4678 let sr = prov.create_resource(&res).unwrap();
4679 assert!(!sr.physical_id.is_empty());
4681 }
4682
4683 #[test]
4684 fn sns_topic_auto_name() {
4685 let prov = make_provisioner();
4686 let res = make_resource("AWS::SNS::Topic", "AutoT", serde_json::json!({}));
4687 let sr = prov.create_resource(&res).unwrap();
4688 assert!(!sr.physical_id.is_empty());
4689 }
4690
4691 #[test]
4694 fn unsupported_resource_type_recorded_with_logical_id() {
4695 let prov = make_provisioner();
4697 let res = make_resource("AWS::FooBar::Thing", "X", serde_json::json!({}));
4698 let sr = prov.create_resource(&res).unwrap();
4699 assert_eq!(sr.physical_id, "X");
4700 assert_eq!(sr.status, "CREATE_COMPLETE");
4701 }
4702
4703 #[test]
4704 fn sqs_queue_with_redrive_policy() {
4705 let prov = make_provisioner();
4706 let dlq = make_resource(
4708 "AWS::SQS::Queue",
4709 "DLQ",
4710 serde_json::json!({"QueueName": "dlq1"}),
4711 );
4712 let dlq_resource = prov.create_resource(&dlq).unwrap();
4713 let _ = dlq_resource.physical_id;
4714
4715 let src = make_resource(
4717 "AWS::SQS::Queue",
4718 "Src",
4719 serde_json::json!({
4720 "QueueName": "src1",
4721 "RedrivePolicy": {
4722 "deadLetterTargetArn": "arn:aws:sqs:us-east-1:123456789012:dlq1",
4723 "maxReceiveCount": 3
4724 }
4725 }),
4726 );
4727 let sr = prov.create_resource(&src).unwrap();
4728 assert!(!sr.physical_id.is_empty());
4729 }
4730
4731 #[test]
4732 fn sns_topic_with_display_name() {
4733 let prov = make_provisioner();
4734 let res = make_resource(
4735 "AWS::SNS::Topic",
4736 "WithName",
4737 serde_json::json!({"TopicName": "named-topic", "DisplayName": "Named"}),
4738 );
4739 let sr = prov.create_resource(&res).unwrap();
4740 assert!(sr.physical_id.contains("named-topic"));
4741 }
4742
4743 #[test]
4744 fn ssm_parameter_with_explicit_name() {
4745 let prov = make_provisioner();
4746 let res = make_resource(
4747 "AWS::SSM::Parameter",
4748 "Param",
4749 serde_json::json!({"Name": "/my/param", "Value": "v", "Type": "String"}),
4750 );
4751 let sr = prov.create_resource(&res).unwrap();
4752 assert!(sr.physical_id.contains("/my/param"));
4753 }
4754
4755 #[test]
4756 fn ssm_parameter_missing_name_errors() {
4757 let prov = make_provisioner();
4758 let res = make_resource(
4759 "AWS::SSM::Parameter",
4760 "AutoP",
4761 serde_json::json!({"Value": "v", "Type": "String"}),
4762 );
4763 assert!(prov.create_resource(&res).is_err());
4764 }
4765
4766 #[test]
4767 fn iam_managed_policy_auto_name() {
4768 let prov = make_provisioner();
4769 let res = make_resource(
4770 "AWS::IAM::Policy",
4771 "AutoPol",
4772 serde_json::json!({
4773 "PolicyName": "inline-pol",
4774 "PolicyDocument": {"Version": "2012-10-17", "Statement": []},
4775 "Users": []
4776 }),
4777 );
4778 let sr = prov.create_resource(&res).unwrap();
4779 assert!(!sr.physical_id.is_empty());
4780 }
4781
4782 #[test]
4783 fn delete_resource_works_for_queue() {
4784 let prov = make_provisioner();
4785 let res = make_resource(
4786 "AWS::SQS::Queue",
4787 "ToDel",
4788 serde_json::json!({"QueueName": "todel"}),
4789 );
4790 let sr = prov.create_resource(&res).unwrap();
4791 assert!(prov.delete_resource(&sr).is_ok());
4792 }
4793
4794 #[test]
4795 fn delete_resource_works_for_topic() {
4796 let prov = make_provisioner();
4797 let res = make_resource(
4798 "AWS::SNS::Topic",
4799 "DelT",
4800 serde_json::json!({"TopicName": "delt"}),
4801 );
4802 let sr = prov.create_resource(&res).unwrap();
4803 assert!(prov.delete_resource(&sr).is_ok());
4804 }
4805
4806 #[test]
4807 fn application_autoscaling_scalable_target_round_trip() {
4808 let prov = make_provisioner();
4809 let res = make_resource(
4810 "AWS::ApplicationAutoScaling::ScalableTarget",
4811 "Target",
4812 serde_json::json!({
4813 "ServiceNamespace": "ecs",
4814 "ResourceId": "service/my-cluster/my-service",
4815 "ScalableDimension": "ecs:service:DesiredCount",
4816 "MinCapacity": 1,
4817 "MaxCapacity": 10,
4818 "RoleARN": "arn:aws:iam::123456789012:role/my-role",
4819 }),
4820 );
4821 let sr = prov.create_resource(&res).unwrap();
4822 assert_eq!(sr.physical_id, "service/my-cluster/my-service");
4823 assert!(sr.attributes.contains_key("ScalableTargetARN"));
4824 assert!(prov.delete_resource(&sr).is_ok());
4825 }
4826
4827 #[test]
4828 fn application_autoscaling_scaling_policy_requires_target() {
4829 let prov = make_provisioner();
4830 let res = make_resource(
4831 "AWS::ApplicationAutoScaling::ScalingPolicy",
4832 "Policy",
4833 serde_json::json!({
4834 "PolicyName": "my-policy",
4835 "ServiceNamespace": "ecs",
4836 "ResourceId": "service/my-cluster/my-service",
4837 "ScalableDimension": "ecs:service:DesiredCount",
4838 "PolicyType": "TargetTrackingScaling",
4839 "TargetTrackingScalingPolicyConfiguration": {
4840 "TargetValue": 50.0,
4841 "PredefinedMetricSpecification": {
4842 "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
4843 }
4844 },
4845 }),
4846 );
4847 assert!(prov.create_resource(&res).is_err());
4849 }
4850
4851 #[test]
4852 fn application_autoscaling_scaling_policy_round_trip() {
4853 let prov = make_provisioner();
4854 let target = make_resource(
4855 "AWS::ApplicationAutoScaling::ScalableTarget",
4856 "Target",
4857 serde_json::json!({
4858 "ServiceNamespace": "ecs",
4859 "ResourceId": "service/my-cluster/my-service",
4860 "ScalableDimension": "ecs:service:DesiredCount",
4861 "MinCapacity": 1,
4862 "MaxCapacity": 10,
4863 }),
4864 );
4865 let sr = prov.create_resource(&target).unwrap();
4866
4867 let policy = make_resource(
4868 "AWS::ApplicationAutoScaling::ScalingPolicy",
4869 "Policy",
4870 serde_json::json!({
4871 "PolicyName": "my-policy",
4872 "ServiceNamespace": "ecs",
4873 "ResourceId": "service/my-cluster/my-service",
4874 "ScalableDimension": "ecs:service:DesiredCount",
4875 "PolicyType": "TargetTrackingScaling",
4876 "TargetTrackingScalingPolicyConfiguration": {
4877 "TargetValue": 50.0,
4878 "PredefinedMetricSpecification": {
4879 "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
4880 }
4881 },
4882 }),
4883 );
4884 let psr = prov.create_resource(&policy).unwrap();
4885 assert!(psr.physical_id.starts_with("arn:aws:autoscaling:"));
4886 assert!(prov.delete_resource(&psr).is_ok());
4887 assert!(prov.delete_resource(&sr).is_ok());
4888 }
4889
4890 #[test]
4891 fn sqs_queue_with_fifo_suffix() {
4892 let prov = make_provisioner();
4893 let res = make_resource(
4894 "AWS::SQS::Queue",
4895 "Fifo",
4896 serde_json::json!({"QueueName": "fq.fifo", "FifoQueue": true}),
4897 );
4898 let sr = prov.create_resource(&res).unwrap();
4899 assert!(sr.physical_id.ends_with(".fifo"));
4900 }
4901
4902 #[test]
4905 fn s3_bucket_provisions_all_config_properties() {
4906 let prov = make_provisioner();
4907 let bucket = make_resource(
4908 "AWS::S3::Bucket",
4909 "MyBucket",
4910 serde_json::json!({
4911 "BucketName": "protected-bucket",
4912 "VersioningConfiguration": { "Status": "Enabled" },
4913 "BucketEncryption": {
4914 "ServerSideEncryptionConfiguration": [{
4915 "ServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256" }
4916 }]
4917 },
4918 "PublicAccessBlockConfiguration": {
4919 "BlockPublicAcls": true,
4920 "BlockPublicPolicy": true,
4921 "IgnorePublicAcls": true,
4922 "RestrictPublicBuckets": true
4923 },
4924 "NotificationConfiguration": {
4925 "QueueConfigurations": [{
4926 "Event": "s3:ObjectCreated:*",
4927 "Queue": "arn:aws:sqs:us-east-1:123456789012:q"
4928 }],
4929 "EventBridgeConfiguration": {}
4930 },
4931 "Tags": [{ "Key": "env", "Value": "prod" }]
4932 }),
4933 );
4934 let sr = prov.create_resource(&bucket).unwrap();
4935 assert_eq!(sr.physical_id, "protected-bucket");
4936
4937 let s3 = prov.s3_state.read();
4938 let acct = s3.get("123456789012").unwrap();
4939 let b = acct.buckets.get("protected-bucket").unwrap();
4940 assert_eq!(b.versioning.as_deref(), Some("Enabled"));
4942 assert!(b
4944 .encryption_config
4945 .as_deref()
4946 .unwrap()
4947 .contains("<SSEAlgorithm>AES256</SSEAlgorithm>"));
4948 assert!(b
4950 .public_access_block
4951 .as_deref()
4952 .unwrap()
4953 .contains("<BlockPublicAcls>true</BlockPublicAcls>"));
4954 assert!(b
4956 .notification_config
4957 .as_deref()
4958 .unwrap()
4959 .contains("<Queue>arn:aws:sqs:us-east-1:123456789012:q</Queue>"));
4960 assert!(b.eventbridge_enabled);
4961 assert_eq!(b.tags.get("env").map(String::as_str), Some("prod"));
4963 }
4964
4965 #[test]
4966 fn s3_bucket_update_enables_versioning() {
4967 let prov = make_provisioner();
4968 let created = prov
4969 .create_resource(&make_resource(
4970 "AWS::S3::Bucket",
4971 "MyBucket",
4972 serde_json::json!({ "BucketName": "later-versioned" }),
4973 ))
4974 .unwrap();
4975 {
4977 let s3 = prov.s3_state.read();
4978 let b = s3
4979 .get("123456789012")
4980 .unwrap()
4981 .buckets
4982 .get("later-versioned")
4983 .unwrap();
4984 assert!(b.versioning.is_none());
4985 }
4986 prov.update_resource(
4988 &created,
4989 &make_resource(
4990 "AWS::S3::Bucket",
4991 "MyBucket",
4992 serde_json::json!({
4993 "BucketName": "later-versioned",
4994 "VersioningConfiguration": { "Status": "Enabled" }
4995 }),
4996 ),
4997 )
4998 .unwrap();
4999 let s3 = prov.s3_state.read();
5000 let b = s3
5001 .get("123456789012")
5002 .unwrap()
5003 .buckets
5004 .get("later-versioned")
5005 .unwrap();
5006 assert_eq!(b.versioning.as_deref(), Some("Enabled"));
5007 }
5008
5009 #[test]
5010 fn sqs_queue_provisions_tags() {
5011 let prov = make_provisioner();
5012 let created = prov
5013 .create_resource(&make_resource(
5014 "AWS::SQS::Queue",
5015 "TaggedQ",
5016 serde_json::json!({
5017 "QueueName": "tagged-q",
5018 "Tags": [
5019 { "Key": "team", "Value": "core" },
5020 { "Key": "env", "Value": "prod" }
5021 ]
5022 }),
5023 ))
5024 .unwrap();
5025 {
5026 let sqs = prov.sqs_state.read();
5027 let q = sqs
5028 .get("123456789012")
5029 .unwrap()
5030 .queues
5031 .get(&created.physical_id)
5032 .unwrap();
5033 assert_eq!(q.tags.get("team").map(String::as_str), Some("core"));
5034 assert_eq!(q.tags.get("env").map(String::as_str), Some("prod"));
5035 }
5036 prov.update_resource(
5038 &created,
5039 &make_resource(
5040 "AWS::SQS::Queue",
5041 "TaggedQ",
5042 serde_json::json!({
5043 "QueueName": "tagged-q",
5044 "Tags": [{ "Key": "team", "Value": "platform" }]
5045 }),
5046 ),
5047 )
5048 .unwrap();
5049 let sqs = prov.sqs_state.read();
5050 let q = sqs
5051 .get("123456789012")
5052 .unwrap()
5053 .queues
5054 .get(&created.physical_id)
5055 .unwrap();
5056 assert_eq!(q.tags.get("team").map(String::as_str), Some("platform"));
5057 assert!(!q.tags.contains_key("env"));
5058 }
5059
5060 #[test]
5063 fn getatt_s3_bucket_arn_returns_arn() {
5064 let prov = make_provisioner();
5065 let bucket = make_resource(
5066 "AWS::S3::Bucket",
5067 "MyBucket",
5068 serde_json::json!({"BucketName": "my-bucket"}),
5069 );
5070 let sr = prov.create_resource(&bucket).unwrap();
5071 assert_eq!(
5072 prov.get_att(&sr, "Arn"),
5073 Some("arn:aws:s3:::my-bucket".to_string())
5074 );
5075 }
5076
5077 #[test]
5078 fn getatt_s3_bucket_domain_name_returns_dns_name() {
5079 let prov = make_provisioner();
5080 let bucket = make_resource(
5081 "AWS::S3::Bucket",
5082 "MyBucket",
5083 serde_json::json!({"BucketName": "my-bucket"}),
5084 );
5085 let sr = prov.create_resource(&bucket).unwrap();
5086 assert_eq!(
5087 prov.get_att(&sr, "DomainName"),
5088 Some("my-bucket.s3.amazonaws.com".to_string())
5089 );
5090 }
5091
5092 #[test]
5093 fn getatt_lambda_function_arn_returns_function_arn() {
5094 let prov = make_provisioner();
5095 let role = make_resource(
5097 "AWS::IAM::Role",
5098 "MyRole",
5099 serde_json::json!({
5100 "RoleName": "my-role",
5101 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
5102 }),
5103 );
5104 let role_sr = prov.create_resource(&role).unwrap();
5105 let fn_res = make_resource(
5106 "AWS::Lambda::Function",
5107 "MyFn",
5108 serde_json::json!({
5109 "FunctionName": "my-fn",
5110 "Runtime": "python3.11",
5111 "Handler": "index.handler",
5112 "Role": role_sr.physical_id,
5113 "Code": {"ZipFile": "def handler(e,c): return e"}
5114 }),
5115 );
5116 let fn_sr = prov.create_resource(&fn_res).unwrap();
5117 let arn = prov.get_att(&fn_sr, "Arn").expect("Arn should resolve");
5118 assert!(arn.starts_with("arn:aws:lambda:"));
5119 assert!(arn.contains(":function:my-fn"));
5120 }
5121
5122 #[test]
5123 fn getatt_iam_role_arn_returns_role_arn() {
5124 let prov = make_provisioner();
5125 let role = make_resource(
5126 "AWS::IAM::Role",
5127 "MyRole",
5128 serde_json::json!({
5129 "RoleName": "my-role",
5130 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
5131 }),
5132 );
5133 let sr = prov.create_resource(&role).unwrap();
5134 assert_eq!(
5135 prov.get_att(&sr, "Arn"),
5136 Some("arn:aws:iam::123456789012:role/my-role".to_string())
5137 );
5138 let role_id = prov.get_att(&sr, "RoleId").expect("RoleId should resolve");
5140 assert!(role_id.starts_with("FKIA"));
5141 }
5142
5143 #[test]
5144 fn getatt_unknown_attribute_returns_none() {
5145 let prov = make_provisioner();
5146 let bucket = make_resource(
5147 "AWS::S3::Bucket",
5148 "MyBucket",
5149 serde_json::json!({"BucketName": "my-bucket"}),
5150 );
5151 let sr = prov.create_resource(&bucket).unwrap();
5152 assert_eq!(prov.get_att(&sr, "NotARealAttr"), None);
5156 }
5157
5158 #[test]
5159 fn getatt_unknown_resource_type_returns_none() {
5160 let prov = make_provisioner();
5161 let stack_resource = StackResource {
5165 logical_id: "Mystery".to_string(),
5166 physical_id: "mystery-id".to_string(),
5167 resource_type: "AWS::Made::Up".to_string(),
5168 status: "CREATE_COMPLETE".to_string(),
5169 service_token: None,
5170 attributes: BTreeMap::new(),
5171 };
5172 assert_eq!(prov.get_att(&stack_resource, "Arn"), None);
5173 }
5174
5175 #[test]
5176 fn getatt_falls_back_to_captured_attributes() {
5177 let prov = make_provisioner();
5178 let stack_resource = StackResource {
5182 logical_id: "MyTopic".to_string(),
5183 physical_id: "arn:aws:sns:us-east-1:123456789012:my-topic".to_string(),
5184 resource_type: "AWS::SNS::Topic".to_string(),
5185 status: "CREATE_COMPLETE".to_string(),
5186 service_token: None,
5187 attributes: {
5188 let mut m = BTreeMap::new();
5189 m.insert("TopicArn".to_string(), "captured-arn".to_string());
5190 m
5191 },
5192 };
5193 assert_eq!(
5194 prov.get_att(&stack_resource, "TopicArn"),
5195 Some("captured-arn".to_string())
5196 );
5197 }
5198
5199 #[test]
5200 fn getatt_mq_broker_endpoints_track_the_live_data_plane() {
5201 let prov = make_provisioner();
5206 let created = prov
5207 .create_resource(&make_resource(
5208 "AWS::AmazonMQ::Broker",
5209 "MyBroker",
5210 serde_json::json!({
5211 "BrokerName": "cfn-live-ep",
5212 "EngineType": "ACTIVEMQ",
5213 "HostInstanceType": "mq.m5.large",
5214 "DeploymentMode": "SINGLE_INSTANCE",
5215 "PubliclyAccessible": false
5216 }),
5217 ))
5218 .expect("create broker");
5219 let broker_id = created.physical_id.clone();
5220 assert!(
5222 created.attributes["AmqpEndpoints"].contains("amazonaws.com"),
5223 "create-time capture is cosmetic: {}",
5224 created.attributes["AmqpEndpoints"]
5225 );
5226 {
5228 let mut g = prov.mq_state.write();
5229 let acct = g.get_or_create("123456789012");
5230 let mut ports = std::collections::BTreeMap::new();
5231 ports.insert("amqp".to_string(), 15672u16);
5232 ports.insert("openwire".to_string(), 15616u16);
5233 acct.data_plane.insert(
5234 broker_id.clone(),
5235 fakecloud_mq::BrokerDataPlane {
5236 container_id: "c-live".to_string(),
5237 host: "127.0.0.1".to_string(),
5238 ports,
5239 },
5240 );
5241 }
5242 let amqp = prov
5244 .get_att(&created, "AmqpEndpoints")
5245 .expect("AmqpEndpoints");
5246 assert!(
5247 amqp.contains("amqp://127.0.0.1:15672") && !amqp.contains("amazonaws.com"),
5248 "GetAtt must project the live data plane, got: {amqp}"
5249 );
5250 let ips = prov.get_att(&created, "IpAddresses").expect("IpAddresses");
5251 assert!(ips.contains("127.0.0.1"), "IpAddresses live: {ips}");
5252 let openwire = prov
5253 .get_att(&created, "OpenWireEndpoints")
5254 .expect("OpenWireEndpoints");
5255 assert!(
5256 openwire.contains("tcp://127.0.0.1:15616"),
5257 "OpenWireEndpoints live: {openwire}"
5258 );
5259 }
5260
5261 #[test]
5262 fn getatt_secrets_manager_arn_resolves_via_live_state() {
5263 let prov = make_provisioner();
5266 let res = make_resource(
5267 "AWS::SecretsManager::Secret",
5268 "MySecret",
5269 serde_json::json!({"Name": "my-secret", "SecretString": "hunter2"}),
5270 );
5271 let sr = prov.create_resource(&res).unwrap();
5272 let arn = prov.get_att(&sr, "Arn").expect("Arn should resolve");
5273 assert!(arn.starts_with("arn:aws:secretsmanager:"));
5274 assert!(arn.ends_with(":secret:my-secret"));
5275 }
5276
5277 #[test]
5278 fn wafv2_web_acl_lifecycle() {
5279 let prov = make_provisioner();
5280 let res = make_resource(
5281 "AWS::WAFv2::WebACL",
5282 "MyAcl",
5283 serde_json::json!({
5284 "Name": "my-acl",
5285 "Scope": "REGIONAL",
5286 "DefaultAction": {"Allow": {}},
5287 "Rules": [{"Name": "rule1", "Priority": 1, "Statement": {}, "VisibilityConfig": {}}],
5288 "VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "my-acl-metric"},
5289 "Capacity": 100,
5290 }),
5291 );
5292 let sr = prov.create_resource(&res).unwrap();
5293 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
5294 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
5295 assert_eq!(prov.get_att(&sr, "Name"), Some("my-acl".to_string()));
5296 assert!(prov.get_att(&sr, "Id").is_some());
5297 assert_eq!(prov.get_att(&sr, "Capacity"), Some("100".to_string()));
5298
5299 prov.delete_resource(&sr.clone()).unwrap();
5300 let fresh = StackResource {
5303 logical_id: "MyAcl".to_string(),
5304 physical_id: sr.physical_id.clone(),
5305 resource_type: "AWS::WAFv2::WebACL".to_string(),
5306 status: "CREATE_COMPLETE".to_string(),
5307 service_token: None,
5308 attributes: BTreeMap::new(),
5309 };
5310 assert_eq!(prov.get_att(&fresh, "Arn"), None);
5311 }
5312
5313 #[test]
5314 fn wafv2_ip_set_lifecycle() {
5315 let prov = make_provisioner();
5316 let res = make_resource(
5317 "AWS::WAFv2::IPSet",
5318 "MyIpSet",
5319 serde_json::json!({
5320 "Name": "my-ipset",
5321 "Scope": "REGIONAL",
5322 "IPAddressVersion": "IPV4",
5323 "Addresses": ["10.0.0.0/8"],
5324 }),
5325 );
5326 let sr = prov.create_resource(&res).unwrap();
5327 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
5328 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
5329 assert_eq!(prov.get_att(&sr, "Name"), Some("my-ipset".to_string()));
5330
5331 prov.delete_resource(&sr.clone()).unwrap();
5332 let fresh = StackResource {
5333 logical_id: "MyIpSet".to_string(),
5334 physical_id: sr.physical_id.clone(),
5335 resource_type: "AWS::WAFv2::IPSet".to_string(),
5336 status: "CREATE_COMPLETE".to_string(),
5337 service_token: None,
5338 attributes: BTreeMap::new(),
5339 };
5340 assert_eq!(prov.get_att(&fresh, "Arn"), None);
5341 }
5342
5343 #[test]
5344 fn wafv2_regex_pattern_set_lifecycle() {
5345 let prov = make_provisioner();
5346 let res = make_resource(
5347 "AWS::WAFv2::RegexPatternSet",
5348 "MyRegexSet",
5349 serde_json::json!({
5350 "Name": "my-regex",
5351 "Scope": "REGIONAL",
5352 "RegularExpressions": [{"RegexString": "^test"}],
5353 }),
5354 );
5355 let sr = prov.create_resource(&res).unwrap();
5356 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
5357 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
5358 assert_eq!(prov.get_att(&sr, "Name"), Some("my-regex".to_string()));
5359
5360 prov.delete_resource(&sr.clone()).unwrap();
5361 let fresh = StackResource {
5362 logical_id: "MyRegexSet".to_string(),
5363 physical_id: sr.physical_id.clone(),
5364 resource_type: "AWS::WAFv2::RegexPatternSet".to_string(),
5365 status: "CREATE_COMPLETE".to_string(),
5366 service_token: None,
5367 attributes: BTreeMap::new(),
5368 };
5369 assert_eq!(prov.get_att(&fresh, "Arn"), None);
5370 }
5371
5372 #[test]
5373 fn wafv2_rule_group_lifecycle() {
5374 let prov = make_provisioner();
5375 let res = make_resource(
5376 "AWS::WAFv2::RuleGroup",
5377 "MyRuleGroup",
5378 serde_json::json!({
5379 "Name": "my-rg",
5380 "Scope": "REGIONAL",
5381 "Capacity": 50,
5382 "Rules": [{"Name": "r1", "Priority": 1, "Statement": {}, "VisibilityConfig": {}}],
5383 "VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "rg-metric"},
5384 }),
5385 );
5386 let sr = prov.create_resource(&res).unwrap();
5387 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
5388 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
5389 assert_eq!(prov.get_att(&sr, "Name"), Some("my-rg".to_string()));
5390
5391 prov.delete_resource(&sr.clone()).unwrap();
5392 let fresh = StackResource {
5393 logical_id: "MyRuleGroup".to_string(),
5394 physical_id: sr.physical_id.clone(),
5395 resource_type: "AWS::WAFv2::RuleGroup".to_string(),
5396 status: "CREATE_COMPLETE".to_string(),
5397 service_token: None,
5398 attributes: BTreeMap::new(),
5399 };
5400 assert_eq!(prov.get_att(&fresh, "Arn"), None);
5401 }
5402
5403 #[test]
5404 fn wafv2_logging_configuration_lifecycle() {
5405 let prov = make_provisioner();
5406 let res = make_resource(
5407 "AWS::WAFv2::LoggingConfiguration",
5408 "MyLogConfig",
5409 serde_json::json!({
5410 "ResourceArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/abc",
5411 "LogDestinationConfigs": ["arn:aws:logs:us-east-1:123456789012:log-group:/aws/waf"],
5412 }),
5413 );
5414 let sr = prov.create_resource(&res).unwrap();
5415 assert_eq!(
5416 sr.physical_id,
5417 "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/abc"
5418 );
5419
5420 prov.delete_resource(&sr.clone()).unwrap();
5421 }
5422
5423 #[test]
5424 fn wafv2_web_acl_association_lifecycle() {
5425 let prov = make_provisioner();
5426 let res = make_resource(
5427 "AWS::WAFv2::WebACLAssociation",
5428 "MyAssoc",
5429 serde_json::json!({
5430 "ResourceArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188",
5431 "WebACLArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/my-acl/abc",
5432 }),
5433 );
5434 let sr = prov.create_resource(&res).unwrap();
5435 assert_eq!(sr.physical_id, "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188");
5436
5437 prov.delete_resource(&sr.clone()).unwrap();
5438 }
5439
5440 #[test]
5441 fn ses_configuration_set_lifecycle() {
5442 let prov = make_provisioner();
5443 let res = make_resource(
5444 "AWS::SES::ConfigurationSet",
5445 "MyConfigSet",
5446 serde_json::json!({
5447 "Name": "my-cs",
5448 "SendingOptions": {"SendingEnabled": true},
5449 "DeliveryOptions": {"TlsPolicy": "REQUIRE"},
5450 }),
5451 );
5452 let sr = prov.create_resource(&res).unwrap();
5453 assert_eq!(sr.physical_id, "my-cs");
5454 assert_eq!(prov.get_att(&sr, "Name"), Some("my-cs".to_string()));
5455
5456 prov.delete_resource(&sr.clone()).unwrap();
5457 let fresh = StackResource {
5458 logical_id: "MyConfigSet".to_string(),
5459 physical_id: "my-cs".to_string(),
5460 resource_type: "AWS::SES::ConfigurationSet".to_string(),
5461 status: "CREATE_COMPLETE".to_string(),
5462 service_token: None,
5463 attributes: BTreeMap::new(),
5464 };
5465 assert_eq!(prov.get_att(&fresh, "Name"), None);
5466 }
5467
5468 #[test]
5469 fn ses_email_identity_lifecycle() {
5470 let prov = make_provisioner();
5471 let res = make_resource(
5472 "AWS::SES::EmailIdentity",
5473 "MyIdentity",
5474 serde_json::json!({"EmailIdentity": "example.com"}),
5475 );
5476 let sr = prov.create_resource(&res).unwrap();
5477 assert_eq!(sr.physical_id, "example.com");
5478 assert_eq!(
5479 prov.get_att(&sr, "IdentityName"),
5480 Some("example.com".to_string())
5481 );
5482
5483 prov.delete_resource(&sr.clone()).unwrap();
5484 let fresh = StackResource {
5485 logical_id: "MyIdentity".to_string(),
5486 physical_id: "example.com".to_string(),
5487 resource_type: "AWS::SES::EmailIdentity".to_string(),
5488 status: "CREATE_COMPLETE".to_string(),
5489 service_token: None,
5490 attributes: BTreeMap::new(),
5491 };
5492 assert_eq!(prov.get_att(&fresh, "IdentityName"), None);
5493 }
5494
5495 #[test]
5496 fn ses_template_lifecycle() {
5497 let prov = make_provisioner();
5498 let res = make_resource(
5499 "AWS::SES::Template",
5500 "MyTemplate",
5501 serde_json::json!({
5502 "Template": {
5503 "TemplateName": "my-tpl",
5504 "SubjectPart": "Hello",
5505 "HtmlPart": "<h1>Hi</h1>",
5506 "TextPart": "Hi",
5507 },
5508 }),
5509 );
5510 let sr = prov.create_resource(&res).unwrap();
5511 assert_eq!(sr.physical_id, "my-tpl");
5512 assert_eq!(
5513 prov.get_att(&sr, "TemplateName"),
5514 Some("my-tpl".to_string())
5515 );
5516
5517 prov.delete_resource(&sr.clone()).unwrap();
5518 let fresh = StackResource {
5519 logical_id: "MyTemplate".to_string(),
5520 physical_id: "my-tpl".to_string(),
5521 resource_type: "AWS::SES::Template".to_string(),
5522 status: "CREATE_COMPLETE".to_string(),
5523 service_token: None,
5524 attributes: BTreeMap::new(),
5525 };
5526 assert_eq!(prov.get_att(&fresh, "TemplateName"), None);
5527 }
5528
5529 #[test]
5530 fn ses_contact_list_lifecycle() {
5531 let prov = make_provisioner();
5532 let res = make_resource(
5533 "AWS::SES::ContactList",
5534 "MyContactList",
5535 serde_json::json!({
5536 "ContactListName": "my-cl",
5537 "Description": "Test contacts",
5538 "Topics": [{"TopicName": "news", "DisplayName": "Newsletter", "Description": "Weekly news"}],
5539 }),
5540 );
5541 let sr = prov.create_resource(&res).unwrap();
5542 assert_eq!(sr.physical_id, "my-cl");
5543 assert_eq!(
5544 prov.get_att(&sr, "ContactListName"),
5545 Some("my-cl".to_string())
5546 );
5547
5548 prov.delete_resource(&sr.clone()).unwrap();
5549 let fresh = StackResource {
5550 logical_id: "MyContactList".to_string(),
5551 physical_id: "my-cl".to_string(),
5552 resource_type: "AWS::SES::ContactList".to_string(),
5553 status: "CREATE_COMPLETE".to_string(),
5554 service_token: None,
5555 attributes: BTreeMap::new(),
5556 };
5557 assert_eq!(prov.get_att(&fresh, "ContactListName"), None);
5558 }
5559
5560 #[test]
5561 fn ses_dedicated_ip_pool_lifecycle() {
5562 let prov = make_provisioner();
5563 let res = make_resource(
5564 "AWS::SES::DedicatedIpPool",
5565 "MyPool",
5566 serde_json::json!({"PoolName": "my-pool", "ScalingMode": "STANDARD"}),
5567 );
5568 let sr = prov.create_resource(&res).unwrap();
5569 assert_eq!(sr.physical_id, "my-pool");
5570 assert_eq!(prov.get_att(&sr, "PoolName"), Some("my-pool".to_string()));
5571
5572 prov.delete_resource(&sr.clone()).unwrap();
5573 let fresh = StackResource {
5574 logical_id: "MyPool".to_string(),
5575 physical_id: "my-pool".to_string(),
5576 resource_type: "AWS::SES::DedicatedIpPool".to_string(),
5577 status: "CREATE_COMPLETE".to_string(),
5578 service_token: None,
5579 attributes: BTreeMap::new(),
5580 };
5581 assert_eq!(prov.get_att(&fresh, "PoolName"), None);
5582 }
5583
5584 #[test]
5585 fn ses_receipt_rule_set_lifecycle() {
5586 let prov = make_provisioner();
5587 let res = make_resource(
5588 "AWS::SES::ReceiptRuleSet",
5589 "MyRuleSet",
5590 serde_json::json!({"RuleSetName": "my-rs"}),
5591 );
5592 let sr = prov.create_resource(&res).unwrap();
5593 assert_eq!(sr.physical_id, "my-rs");
5594 assert_eq!(prov.get_att(&sr, "RuleSetName"), Some("my-rs".to_string()));
5595
5596 prov.delete_resource(&sr.clone()).unwrap();
5597 let fresh = StackResource {
5598 logical_id: "MyRuleSet".to_string(),
5599 physical_id: "my-rs".to_string(),
5600 resource_type: "AWS::SES::ReceiptRuleSet".to_string(),
5601 status: "CREATE_COMPLETE".to_string(),
5602 service_token: None,
5603 attributes: BTreeMap::new(),
5604 };
5605 assert_eq!(prov.get_att(&fresh, "RuleSetName"), None);
5606 }
5607
5608 #[test]
5609 fn ses_receipt_rule_lifecycle() {
5610 let prov = make_provisioner();
5611 let rs = make_resource(
5612 "AWS::SES::ReceiptRuleSet",
5613 "MyRuleSet",
5614 serde_json::json!({"RuleSetName": "my-rs2"}),
5615 );
5616 prov.create_resource(&rs).unwrap();
5617
5618 let res = make_resource(
5619 "AWS::SES::ReceiptRule",
5620 "MyRule",
5621 serde_json::json!({
5622 "RuleSetName": "my-rs2",
5623 "Rule": {
5624 "Name": "rule1",
5625 "Priority": 1,
5626 "Enabled": true,
5627 "Actions": [{"S3Action": {"BucketName": "my-bucket"}}],
5628 },
5629 }),
5630 );
5631 let sr = prov.create_resource(&res).unwrap();
5632 assert_eq!(sr.physical_id, "my-rs2|rule1");
5633
5634 prov.delete_resource(&sr.clone()).unwrap();
5635 }
5636
5637 #[test]
5638 fn ses_receipt_filter_lifecycle() {
5639 let prov = make_provisioner();
5640 let res = make_resource(
5641 "AWS::SES::ReceiptFilter",
5642 "MyFilter",
5643 serde_json::json!({
5644 "Filter": {
5645 "Name": "my-filter",
5646 "IpFilter": {"Policy": "Block", "Cidr": "10.0.0.0/8"},
5647 },
5648 }),
5649 );
5650 let sr = prov.create_resource(&res).unwrap();
5651 assert_eq!(sr.physical_id, "my-filter");
5652
5653 prov.delete_resource(&sr.clone()).unwrap();
5654 }
5655
5656 #[test]
5657 fn ses_vdm_attributes_lifecycle() {
5658 let prov = make_provisioner();
5659 let res = make_resource(
5660 "AWS::SES::VdmAttributes",
5661 "MyVdm",
5662 serde_json::json!({
5663 "DashboardAttributes": {"EngagementMetrics": "ENABLED"},
5664 "GuardianAttributes": {"OptimizedSharedDelivery": "ENABLED"},
5665 }),
5666 );
5667 let sr = prov.create_resource(&res).unwrap();
5668 assert_eq!(sr.physical_id, "vdm-MyVdm");
5669
5670 prov.delete_resource(&sr.clone()).unwrap();
5671 }
5672
5673 #[test]
5674 fn athena_work_group_lifecycle() {
5675 let prov = make_provisioner();
5676 let res = make_resource(
5677 "AWS::Athena::WorkGroup",
5678 "MyWg",
5679 serde_json::json!({
5680 "Name": "my-wg",
5681 "Description": "test wg",
5682 "Configuration": {"EnforceWorkGroupConfiguration": true},
5683 }),
5684 );
5685 let sr = prov.create_resource(&res).unwrap();
5686 assert_eq!(sr.physical_id, "my-wg");
5687 assert_eq!(sr.attributes.get("Name"), Some(&"my-wg".to_string()));
5688 assert!(sr
5689 .attributes
5690 .get("Arn")
5691 .unwrap()
5692 .contains("workgroup/my-wg"));
5693
5694 assert_eq!(
5695 prov.get_att(
5696 &StackResource {
5697 resource_type: "AWS::Athena::WorkGroup".to_string(),
5698 physical_id: sr.physical_id.clone(),
5699 logical_id: "MyWg".to_string(),
5700 status: "CREATE_COMPLETE".to_string(),
5701 service_token: None,
5702 attributes: BTreeMap::new(),
5703 },
5704 "Name",
5705 ),
5706 Some("my-wg".to_string()),
5707 );
5708
5709 prov.delete_resource(&sr.clone()).unwrap();
5710 }
5711
5712 #[test]
5713 fn athena_data_catalog_lifecycle() {
5714 let prov = make_provisioner();
5715 let res = make_resource(
5716 "AWS::Athena::DataCatalog",
5717 "MyCatalog",
5718 serde_json::json!({
5719 "Name": "my-catalog",
5720 "Type": "GLUE",
5721 "Description": "test catalog",
5722 }),
5723 );
5724 let sr = prov.create_resource(&res).unwrap();
5725 assert_eq!(sr.physical_id, "my-catalog");
5726 assert_eq!(sr.attributes.get("Name"), Some(&"my-catalog".to_string()));
5727 assert!(sr
5728 .attributes
5729 .get("Arn")
5730 .unwrap()
5731 .contains("datacatalog/my-catalog"));
5732
5733 prov.delete_resource(&sr.clone()).unwrap();
5734 }
5735
5736 #[test]
5737 fn athena_named_query_lifecycle() {
5738 let prov = make_provisioner();
5739 let res = make_resource(
5740 "AWS::Athena::NamedQuery",
5741 "MyQuery",
5742 serde_json::json!({
5743 "Name": "my-query",
5744 "Database": "mydb",
5745 "QueryString": "SELECT 1",
5746 "WorkGroup": "primary",
5747 }),
5748 );
5749 let sr = prov.create_resource(&res).unwrap();
5750 assert!(!sr.physical_id.is_empty());
5751 assert_eq!(sr.attributes.get("NamedQueryId"), Some(&sr.physical_id));
5752
5753 prov.delete_resource(&sr.clone()).unwrap();
5754 }
5755
5756 #[test]
5757 fn athena_prepared_statement_lifecycle() {
5758 let prov = make_provisioner();
5759 let res = make_resource(
5760 "AWS::Athena::PreparedStatement",
5761 "MyPs",
5762 serde_json::json!({
5763 "StatementName": "my-ps",
5764 "WorkGroupName": "primary",
5765 "QueryStatement": "SELECT 1",
5766 }),
5767 );
5768 let sr = prov.create_resource(&res).unwrap();
5769 assert_eq!(sr.physical_id, "primary|my-ps");
5770
5771 prov.delete_resource(&sr.clone()).unwrap();
5772 }
5773
5774 #[test]
5775 fn parse_lambda_function_name_handles_every_shape() {
5776 assert_eq!(parse_lambda_function_name("my-func"), "my-func");
5778 assert_eq!(parse_lambda_function_name("my-func:live"), "my-func");
5781 assert_eq!(parse_lambda_function_name("my-func:42"), "my-func");
5782 assert_eq!(
5784 parse_lambda_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-func"),
5785 "my-func"
5786 );
5787 assert_eq!(
5788 parse_lambda_function_name(
5789 "arn:aws:lambda:us-east-1:123456789012:function:my-func:live"
5790 ),
5791 "my-func"
5792 );
5793 assert_eq!(
5795 parse_lambda_function_name("123456789012:function:my-func"),
5796 "my-func"
5797 );
5798 assert_eq!(
5799 parse_lambda_function_name("123456789012:function:my-func:live"),
5800 "my-func"
5801 );
5802 }
5803
5804 #[test]
5805 fn alias_state_key_recovers_internal_key_from_arn() {
5806 assert_eq!(
5808 alias_state_key("arn:aws:lambda:us-east-1:123456789012:function:my-func:live"),
5809 "my-func:live"
5810 );
5811 assert_eq!(alias_state_key("my-func:live"), "my-func:live");
5813 }
5814
5815 #[test]
5818 fn cfn_mq_broker_creates_declared_users() {
5819 let prov = make_provisioner();
5822 let created = prov
5823 .create_resource(&make_resource(
5824 "AWS::AmazonMQ::Broker",
5825 "B",
5826 serde_json::json!({
5827 "BrokerName": "cfn-users",
5828 "EngineType": "ACTIVEMQ",
5829 "DeploymentMode": "SINGLE_INSTANCE",
5830 "HostInstanceType": "mq.m5.large",
5831 "PubliclyAccessible": false,
5832 "Users": [ { "Username": "admin", "Password": "SuperSecret1234" } ]
5833 }),
5834 ))
5835 .expect("broker provisions");
5836
5837 let guard = prov.mq_state.read();
5838 let acct = guard.get("123456789012").unwrap();
5839 let users = acct
5840 .users
5841 .get(&created.physical_id)
5842 .expect("broker has a users map");
5843 assert!(users.contains_key("admin"), "declared user was created");
5844 }
5845
5846 #[test]
5847 fn cfn_mq_broker_synthesizes_subnets_matching_api_path() {
5848 let prov = make_provisioner();
5851 let created = prov
5852 .create_resource(&make_resource(
5853 "AWS::AmazonMQ::Broker",
5854 "B",
5855 serde_json::json!({
5856 "BrokerName": "cfn-subnets",
5857 "EngineType": "ACTIVEMQ",
5858 "DeploymentMode": "SINGLE_INSTANCE",
5859 "HostInstanceType": "mq.m5.large",
5860 "PubliclyAccessible": false
5861 }),
5862 ))
5863 .expect("broker provisions");
5864
5865 let guard = prov.mq_state.read();
5866 let acct = guard.get("123456789012").unwrap();
5867 let broker = acct.brokers.get(&created.physical_id).unwrap();
5868 let subnets: Vec<String> = broker["subnetIds"]
5869 .as_array()
5870 .unwrap()
5871 .iter()
5872 .map(|v| v.as_str().unwrap().to_string())
5873 .collect();
5874 let expected: Vec<String> =
5875 fakecloud_mq::shared::synthesize_subnets(&created.physical_id, "SINGLE_INSTANCE")
5876 .into_iter()
5877 .map(|v| v.as_str().unwrap().to_string())
5878 .collect();
5879 assert!(!subnets.is_empty(), "subnets are synthesized, not empty");
5880 assert_eq!(subnets, expected, "CFN subnets match the shared API path");
5881 }
5882
5883 #[test]
5884 fn cfn_mq_configuration_association_preserves_history() {
5885 let prov = make_provisioner();
5888 let broker = prov
5889 .create_resource(&make_resource(
5890 "AWS::AmazonMQ::Broker",
5891 "B",
5892 serde_json::json!({
5893 "BrokerName": "cfn-hist",
5894 "EngineType": "ACTIVEMQ",
5895 "DeploymentMode": "SINGLE_INSTANCE",
5896 "HostInstanceType": "mq.m5.large",
5897 "PubliclyAccessible": false
5898 }),
5899 ))
5900 .expect("broker provisions");
5901
5902 let prior_id = {
5904 let guard = prov.mq_state.read();
5905 let acct = guard.get("123456789012").unwrap();
5906 acct.brokers.get(&broker.physical_id).unwrap()["configurations"]["current"]["id"]
5907 .as_str()
5908 .unwrap()
5909 .to_string()
5910 };
5911
5912 prov.create_resource(&make_resource(
5913 "AWS::AmazonMQ::ConfigurationAssociation",
5914 "A",
5915 serde_json::json!({
5916 "Broker": broker.physical_id,
5917 "Configuration": { "Id": "c-new-config", "Revision": 2 }
5918 }),
5919 ))
5920 .expect("association provisions");
5921
5922 let guard = prov.mq_state.read();
5923 let acct = guard.get("123456789012").unwrap();
5924 let configs = &acct.brokers.get(&broker.physical_id).unwrap()["configurations"];
5925 assert_eq!(configs["current"]["id"].as_str(), Some("c-new-config"));
5926 assert_eq!(configs["current"]["revision"].as_i64(), Some(2));
5927 let history = configs["history"].as_array().unwrap();
5928 assert!(
5929 history
5930 .iter()
5931 .any(|h| h["id"].as_str() == Some(prior_id.as_str())),
5932 "prior current configuration is preserved in history"
5933 );
5934 }
5935
5936 #[test]
5937 fn config_rule_getatt_config_rule_id_resolves() {
5938 let prov = make_provisioner();
5939 let created = prov
5940 .create_resource(&make_resource(
5941 "AWS::Config::ConfigRule",
5942 "MyRule",
5943 serde_json::json!({
5944 "ConfigRuleName": "s3-versioning",
5945 "Source": { "Owner": "AWS", "SourceIdentifier": "S3_BUCKET_VERSIONING_ENABLED" },
5946 }),
5947 ))
5948 .expect("provision config rule");
5949 let stored_id = prov
5951 .config_state
5952 .read()
5953 .account("123456789012")
5954 .and_then(|a| a.rules.get("s3-versioning").map(|r| r.rule_id.clone()))
5955 .expect("rule stored");
5956 let got = created.attributes.get("ConfigRuleId").cloned();
5959 assert_eq!(got.as_deref(), Some(stored_id.as_str()));
5960 assert!(!stored_id.is_empty());
5961 assert_eq!(
5962 created.attributes.get("ComplianceType").map(String::as_str),
5963 Some("INSUFFICIENT_DATA")
5964 );
5965 assert!(created.attributes.contains_key("Arn"));
5966 }
5967
5968 #[test]
5973 fn route53resolver_endpoint_host_vpc_and_config_getatt() {
5974 let prov = make_provisioner();
5975 let vpc = prov
5976 .create_resource(&make_resource(
5977 "AWS::EC2::VPC",
5978 "Vpc",
5979 serde_json::json!({ "CidrBlock": "10.0.0.0/16" }),
5980 ))
5981 .expect("vpc provisions");
5982 let vpc_id = vpc.physical_id.clone();
5983 let subnet = prov
5984 .create_resource(&make_resource(
5985 "AWS::EC2::Subnet",
5986 "Subnet",
5987 serde_json::json!({
5988 "VpcId": vpc_id,
5989 "CidrBlock": "10.0.1.0/24",
5990 "AvailabilityZone": "us-east-1a",
5991 }),
5992 ))
5993 .expect("subnet provisions");
5994 let subnet_id = subnet.physical_id.clone();
5995
5996 let endpoint = prov
5997 .create_resource(&make_resource(
5998 "AWS::Route53Resolver::ResolverEndpoint",
5999 "Endpoint",
6000 serde_json::json!({
6001 "Direction": "OUTBOUND",
6002 "SecurityGroupIds": ["sg-1"],
6003 "IpAddresses": [{ "SubnetId": subnet_id }],
6004 }),
6005 ))
6006 .expect("endpoint provisions");
6007 assert_eq!(
6009 endpoint.attributes.get("HostVPCId").map(String::as_str),
6010 Some(vpc_id.as_str())
6011 );
6012 assert_eq!(
6013 prov.get_att(&endpoint, "HostVPCId").as_deref(),
6014 Some(vpc_id.as_str())
6015 );
6016
6017 let cfg = prov
6019 .create_resource(&make_resource(
6020 "AWS::Route53Resolver::ResolverConfig",
6021 "Cfg",
6022 serde_json::json!({ "ResourceId": vpc_id, "AutodefinedReverseFlag": "DISABLE" }),
6023 ))
6024 .expect("resolver config provisions");
6025 assert_eq!(
6026 prov.get_att(&cfg, "ResourceId").as_deref(),
6027 Some(vpc_id.as_str())
6028 );
6029 assert_eq!(
6030 prov.get_att(&cfg, "OwnerId").as_deref(),
6031 Some("123456789012")
6032 );
6033 assert!(prov.get_att(&cfg, "Id").is_some());
6034 }
6035}