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_apigateway::{
13 make_id as apigw_make_id, ApiKey as ApiGwApiKey, Authorizer as ApiGwAuthorizer,
14 Deployment as ApiGwDeployment, Integration as ApiGwIntegration, Method as ApiGwMethod,
15 Model as ApiGwModel, Resource as ApiGwResource, RestApi as ApiGwRestApi, SharedApiGatewayState,
16 Stage as ApiGwStage, UsagePlan as ApiGwUsagePlan,
17};
18use fakecloud_apigatewayv2::{
19 Authorizer as ApiGwV2Authorizer, CorsConfiguration as ApiGwV2CorsConfiguration,
20 Deployment as ApiGwV2Deployment, HttpApi as ApiGwV2HttpApi, Integration as ApiGwV2Integration,
21 JwtConfiguration as ApiGwV2JwtConfiguration, Route as ApiGwV2Route, SharedApiGatewayV2State,
22 Stage as ApiGwV2Stage,
23};
24use fakecloud_application_autoscaling::{
25 ScalableTarget as AppasScalableTarget, ScalingPolicy as AppasScalingPolicy,
26 SharedApplicationAutoScalingState as AppasState, SuspendedState as AppasSuspendedState,
27};
28use fakecloud_athena::{DataCatalog, NamedQuery, PreparedStatement, SharedAthenaState, WorkGroup};
29use fakecloud_aws::arn::Arn;
30use fakecloud_cloudfront::{
31 functions::{
32 CloudFrontOriginAccessIdentityConfig, FunctionConfig, KeyGroupConfig, KeyGroupItems,
33 PublicKeyConfig, StoredFunction, StoredKeyGroup, StoredOriginAccessIdentity,
34 StoredPublicKey,
35 },
36 model::{
37 DefaultCacheBehavior, DistributionConfig, Origin, OriginItems, Origins, ViewerCertificate,
38 },
39 policies::{
40 CachePolicyConfig, OriginAccessControlConfig, OriginRequestPolicyConfig,
41 OriginRequestPolicyCookiesConfig, OriginRequestPolicyHeadersConfig,
42 OriginRequestPolicyQueryStringsConfig, ResponseHeadersPolicyConfig, StoredCachePolicy,
43 StoredOriginAccessControl, StoredOriginRequestPolicy, StoredResponseHeadersPolicy,
44 },
45 SharedCloudFrontState, StoredDistribution,
46};
47use fakecloud_cloudwatch::{AlarmState, Dashboard, MetricAlarm, SharedCloudWatchState};
48use fakecloud_cognito::{
49 default_schema_attributes, AccountRecoverySetting, AdminCreateUserConfig,
50 CognitoIdentityProvider, CustomDomainConfig, EmailConfiguration, IdentityPool,
51 IdentityPoolRoleAttachment, PasswordPolicy, PoolPolicies, RecoveryOption, SchemaAttribute,
52 SharedCognitoState, SignInPolicy, SmsConfiguration, UserPool, UserPoolClient, UserPoolDomain,
53};
54use fakecloud_core::delivery::DeliveryBus;
55use fakecloud_dynamodb::{
56 AttributeDefinition, DynamoTable, KeySchemaElement, OnDemandThroughput, ProvisionedThroughput,
57 SharedDynamoDbState,
58};
59use fakecloud_ecr::{Repository, SharedEcrState};
60use fakecloud_ecs::{
61 CapacityProvider as EcsCapacityProvider, Cluster as EcsCluster, Service as EcsService,
62 SharedEcsState, TagEntry as EcsTagEntry, TaskDefinition as EcsTaskDefinition,
63};
64use fakecloud_elasticache::{
65 CacheCluster as EcCacheCluster, CacheParameterGroup, CacheSecurityGroup, CacheSubnetGroup,
66 ElastiCacheUser as EcUser, ElastiCacheUserGroup as EcUserGroup,
67 ReplicationGroup as EcReplicationGroup, SharedElastiCacheState,
68};
69use fakecloud_elbv2::{
70 Action as ElbAction, Listener, LoadBalancer, Rule as ElbRule, RuleCondition, SharedElbv2State,
71 Tag as ElbTag, TargetGroup, TargetGroupTuple,
72};
73use fakecloud_eventbridge::{
74 ApiDestination, Archive, Connection, Endpoint, EventBus, EventRule, SharedEventBridgeState,
75};
76use fakecloud_firehose::{DeliveryStream, S3Destination};
77use fakecloud_iam::{
78 IamAccessKey, IamGroup, IamInstanceProfile, IamPolicy, IamRole, IamUser, OidcProvider,
79 PolicyVersion, SamlProvider, SharedIamState, Tag, VirtualMfaDevice,
80};
81use fakecloud_kinesis::{build_stream_shards, KinesisConsumer, KinesisStream, SharedKinesisState};
82use fakecloud_kms::provisioner as kms_provisioner;
83use fakecloud_kms::SharedKmsState;
84use fakecloud_lambda::{
85 AttachedLayer, EventSourceMapping, FunctionAlias, FunctionUrlConfig, Layer, LayerVersion,
86 SharedLambdaState,
87};
88use fakecloud_logs::{
89 Delivery, DeliveryDestination, DeliverySource, Destination, LogStream, MetricFilter,
90 MetricTransformation, QueryDefinition, ResourcePolicy, SharedLogsState, SubscriptionFilter,
91};
92use fakecloud_organizations::{
93 OrganizationState, OrganizationalUnit, Policy as OrgPolicy, SharedOrganizationsState,
94 POLICY_TYPE_SCP,
95};
96use fakecloud_persistence::{BucketSubresource, S3Store};
97use fakecloud_rds::{DbInstance, DbParameterGroup, DbSubnetGroup, RdsTag, SharedRdsState};
98use fakecloud_route53::{
99 model::{HealthCheckConfig, HostedZoneFeatures, ResourceRecordSet},
100 SharedRoute53State, StoredHealthCheck, StoredHostedZone,
101};
102use fakecloud_s3::persistence::bucket_meta_snapshot;
103use fakecloud_s3::{S3Bucket, SharedS3State};
104use fakecloud_secretsmanager::{RotationRules, Secret, SecretVersion, SharedSecretsManagerState};
105use fakecloud_ses::{
106 ConfigurationSet as SesConfigurationSet, ContactList as SesContactList,
107 DedicatedIpPool as SesDedicatedIpPool, EmailIdentity as SesEmailIdentity,
108 EmailTemplate as SesEmailTemplate, EventDestination as SesEventDestination,
109 IpFilter as SesIpFilter, ReceiptAction as SesReceiptAction, ReceiptFilter as SesReceiptFilter,
110 ReceiptRule as SesReceiptRule, ReceiptRuleSet as SesReceiptRuleSet, SharedSesState,
111};
112use fakecloud_sns::{SharedSnsState, SnsSubscription, SnsTopic};
113use fakecloud_sqs::{SharedSqsState, SqsQueue};
114use fakecloud_ssm::{SharedSsmState, SsmParameter};
115use fakecloud_stepfunctions::{
116 Activity as SfnActivity, AliasRoute, SharedStepFunctionsState, StateMachine, StateMachineAlias,
117 StateMachineStatus, StateMachineType, StateMachineVersion,
118};
119use fakecloud_wafv2::{IpSet, RegexPatternSet, RuleGroup, SharedWafv2State, WebAcl};
120
121use crate::state::StackResource;
122use crate::template::ResourceDefinition;
123
124fn parse_iam_tags(value: Option<&serde_json::Value>) -> Vec<Tag> {
128 let Some(arr) = value.and_then(|v| v.as_array()) else {
129 return Vec::new();
130 };
131 arr.iter()
132 .filter_map(|t| {
133 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
134 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
135 Some(Tag { key, value })
136 })
137 .collect()
138}
139
140fn parse_elb_tags(value: Option<&serde_json::Value>) -> Vec<ElbTag> {
143 let Some(arr) = value.and_then(|v| v.as_array()) else {
144 return Vec::new();
145 };
146 arr.iter()
147 .filter_map(|t| {
148 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
149 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
150 Some(ElbTag { key, value })
151 })
152 .collect()
153}
154
155fn parse_elb_actions(value: Option<&serde_json::Value>) -> Vec<ElbAction> {
159 let Some(arr) = value.and_then(|v| v.as_array()) else {
160 return Vec::new();
161 };
162 arr.iter()
163 .map(|a| {
164 let action_type = a
165 .get("Type")
166 .and_then(|v| v.as_str())
167 .unwrap_or("forward")
168 .to_string();
169 let target_group_arn = a
170 .get("TargetGroupArn")
171 .and_then(|v| v.as_str())
172 .map(|s| s.to_string());
173 let order = a.get("Order").and_then(|v| v.as_i64()).map(|n| n as i32);
174 let redirect = a
175 .get("RedirectConfig")
176 .map(|r| fakecloud_elbv2::RedirectConfig {
177 protocol: r
178 .get("Protocol")
179 .and_then(|v| v.as_str())
180 .map(|s| s.to_string()),
181 port: r
182 .get("Port")
183 .and_then(|v| v.as_str())
184 .map(|s| s.to_string()),
185 host: r
186 .get("Host")
187 .and_then(|v| v.as_str())
188 .map(|s| s.to_string()),
189 path: r
190 .get("Path")
191 .and_then(|v| v.as_str())
192 .map(|s| s.to_string()),
193 query: r
194 .get("Query")
195 .and_then(|v| v.as_str())
196 .map(|s| s.to_string()),
197 status_code: r
198 .get("StatusCode")
199 .and_then(|v| v.as_str())
200 .unwrap_or("HTTP_302")
201 .to_string(),
202 });
203 let fixed_response =
204 a.get("FixedResponseConfig")
205 .map(|f| fakecloud_elbv2::FixedResponseConfig {
206 message_body: f
207 .get("MessageBody")
208 .and_then(|v| v.as_str())
209 .map(|s| s.to_string()),
210 status_code: f
211 .get("StatusCode")
212 .and_then(|v| v.as_str())
213 .unwrap_or("200")
214 .to_string(),
215 content_type: f
216 .get("ContentType")
217 .and_then(|v| v.as_str())
218 .map(|s| s.to_string()),
219 });
220 let forward = a.get("ForwardConfig").map(|f| {
221 let target_groups: Vec<TargetGroupTuple> = f
222 .get("TargetGroups")
223 .and_then(|v| v.as_array())
224 .map(|arr| {
225 arr.iter()
226 .filter_map(|t| {
227 let target_group_arn = t
228 .get("TargetGroupArn")
229 .and_then(|v| v.as_str())?
230 .to_string();
231 let weight =
232 t.get("Weight").and_then(|v| v.as_i64()).map(|n| n as i32);
233 Some(TargetGroupTuple {
234 target_group_arn,
235 weight,
236 })
237 })
238 .collect()
239 })
240 .unwrap_or_default();
241 fakecloud_elbv2::ForwardConfig {
242 target_groups,
243 stickiness: None,
244 }
245 });
246 ElbAction {
247 action_type,
248 target_group_arn,
249 order,
250 redirect,
251 fixed_response,
252 forward,
253 authenticate_cognito: None,
254 authenticate_oidc: None,
255 }
256 })
257 .collect()
258}
259
260fn parse_elb_rule_conditions(value: Option<&serde_json::Value>) -> Vec<RuleCondition> {
261 let Some(arr) = value.and_then(|v| v.as_array()) else {
262 return Vec::new();
263 };
264 arr.iter()
265 .map(|c| {
266 let field = c
267 .get("Field")
268 .and_then(|v| v.as_str())
269 .unwrap_or("")
270 .to_string();
271 let values: Vec<String> = c
272 .get("Values")
273 .and_then(|v| v.as_array())
274 .map(|arr| {
275 arr.iter()
276 .filter_map(|s| s.as_str().map(|s| s.to_string()))
277 .collect()
278 })
279 .unwrap_or_default();
280 let host_header_values: Vec<String> = c
281 .get("HostHeaderConfig")
282 .and_then(|v| v.get("Values"))
283 .and_then(|v| v.as_array())
284 .map(|arr| {
285 arr.iter()
286 .filter_map(|s| s.as_str().map(|s| s.to_string()))
287 .collect()
288 })
289 .unwrap_or_default();
290 RuleCondition {
291 field,
292 values,
293 host_header_values,
294 path_pattern_values: Vec::new(),
295 http_header_name: None,
296 http_header_values: Vec::new(),
297 query_string_values: Vec::new(),
298 http_request_method_values: Vec::new(),
299 source_ip_values: Vec::new(),
300 }
301 })
302 .collect()
303}
304
305fn parse_key_policy(props: &serde_json::Value) -> Option<String> {
310 match props.get("KeyPolicy") {
311 Some(v) if v.is_string() => Some(v.as_str().unwrap_or("").to_string()),
312 Some(v) => Some(serde_json::to_string(v).unwrap_or_default()),
313 None => None,
314 }
315}
316
317fn parse_tag_list(props: &serde_json::Value) -> BTreeMap<String, String> {
320 let mut tags: BTreeMap<String, String> = BTreeMap::new();
321 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
322 for t in arr {
323 if let (Some(k), Some(v)) = (
324 t.get("Key").and_then(|x| x.as_str()),
325 t.get("Value").and_then(|x| x.as_str()),
326 ) {
327 tags.insert(k.to_string(), v.to_string());
328 }
329 }
330 }
331 tags
332}
333
334fn parse_kms_key_input(props: &serde_json::Value) -> kms_provisioner::KeyCreationInput {
344 kms_provisioner::KeyCreationInput {
345 description: props
346 .get("Description")
347 .and_then(|v| v.as_str())
348 .unwrap_or("")
349 .to_string(),
350 key_usage: props
351 .get("KeyUsage")
352 .and_then(|v| v.as_str())
353 .unwrap_or("ENCRYPT_DECRYPT")
354 .to_string(),
355 key_spec: props
356 .get("KeySpec")
357 .and_then(|v| v.as_str())
358 .unwrap_or("SYMMETRIC_DEFAULT")
359 .to_string(),
360 origin: props
361 .get("Origin")
362 .and_then(|v| v.as_str())
363 .unwrap_or("AWS_KMS")
364 .to_string(),
365 enabled: props
366 .get("Enabled")
367 .and_then(|v| v.as_bool())
368 .unwrap_or(true),
369 multi_region: props
370 .get("MultiRegion")
371 .and_then(|v| v.as_bool())
372 .unwrap_or(false),
373 key_rotation_enabled: props
374 .get("EnableKeyRotation")
375 .and_then(|v| v.as_bool())
376 .unwrap_or(false),
377 policy: parse_key_policy(props),
378 tags: parse_tag_list(props),
379 }
380}
381
382fn parse_log_group_name(input: &str) -> String {
386 if let Some(rest) = input.strip_prefix("arn:aws:logs:") {
387 if let Some(after) = rest.split(":log-group:").nth(1) {
388 return after.trim_end_matches(":*").to_string();
390 }
391 }
392 input.to_string()
393}
394
395fn parse_lambda_function_name(input: &str) -> String {
403 if let Some(rest) = input.strip_prefix("arn:aws:lambda:") {
405 if let Some(after) = rest.split(":function:").nth(1) {
406 return after.split(':').next().unwrap_or(after).to_string();
407 }
408 }
409 if let Some(after) = input.split(":function:").nth(1) {
411 return after.split(':').next().unwrap_or(after).to_string();
412 }
413 input.split(':').next().unwrap_or(input).to_string()
415}
416
417fn alias_state_key(physical_id: &str) -> String {
423 if let Some(rest) = physical_id.strip_prefix("arn:aws:lambda:") {
424 if let Some(after) = rest.split(":function:").nth(1) {
425 return after.to_string();
426 }
427 }
428 physical_id.to_string()
429}
430
431struct LambdaFunctionProps {
435 runtime: String,
436 role: String,
437 handler: String,
438 description: String,
439 timeout: i64,
440 memory_size: i64,
441 package_type: String,
442 tags: BTreeMap<String, String>,
443 environment: BTreeMap<String, String>,
444 architectures: Vec<String>,
445 code_zip: Option<Vec<u8>>,
449 s3_bucket: Option<String>,
450 s3_key: Option<String>,
451 image_uri: Option<String>,
452 layers: Vec<String>,
453 tracing_mode: Option<String>,
454 kms_key_arn: Option<String>,
455 ephemeral_storage_size: Option<i64>,
456 vpc_config: Option<serde_json::Value>,
457 snap_start: Option<serde_json::Value>,
458 dead_letter_config_arn: Option<String>,
459 file_system_configs: Vec<serde_json::Value>,
460 logging_config: Option<serde_json::Value>,
461}
462
463fn parse_lambda_function_props(props: &serde_json::Value) -> Result<LambdaFunctionProps, String> {
468 let runtime = props
469 .get("Runtime")
470 .and_then(|v| v.as_str())
471 .unwrap_or("python3.12")
472 .to_string();
473 let role = props
474 .get("Role")
475 .and_then(|v| v.as_str())
476 .unwrap_or_default()
477 .to_string();
478 let handler = props
479 .get("Handler")
480 .and_then(|v| v.as_str())
481 .unwrap_or("index.handler")
482 .to_string();
483 let description = props
484 .get("Description")
485 .and_then(|v| v.as_str())
486 .unwrap_or_default()
487 .to_string();
488 let timeout = props.get("Timeout").and_then(|v| v.as_i64()).unwrap_or(3);
489 let memory_size = props
490 .get("MemorySize")
491 .and_then(|v| v.as_i64())
492 .unwrap_or(128);
493 let architectures = props
494 .get("Architectures")
495 .and_then(|v| v.as_array())
496 .map(|a| {
497 a.iter()
498 .filter_map(|v| v.as_str().map(|s| s.to_string()))
499 .collect::<Vec<_>>()
500 })
501 .unwrap_or_else(|| vec!["x86_64".to_string()]);
502 let package_type = props
503 .get("PackageType")
504 .and_then(|v| v.as_str())
505 .unwrap_or("Zip")
506 .to_string();
507 let environment = props
508 .get("Environment")
509 .and_then(|v| v.get("Variables"))
510 .and_then(|v| v.as_object())
511 .map(|o| {
512 o.iter()
513 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
514 .collect::<BTreeMap<String, String>>()
515 })
516 .unwrap_or_default();
517
518 let tags: BTreeMap<String, String> = props
521 .get("Tags")
522 .and_then(|v| v.as_array())
523 .map(|arr| {
524 arr.iter()
525 .filter_map(|t| {
526 let k = t.get("Key").and_then(|v| v.as_str())?.to_string();
527 let v = t.get("Value").and_then(|v| v.as_str())?.to_string();
528 Some((k, v))
529 })
530 .collect()
531 })
532 .unwrap_or_default();
533
534 let code = props.get("Code");
535 let code_zip = code
541 .and_then(|c| c.get("ZipFile"))
542 .and_then(|v| v.as_str())
543 .map(|s| s.as_bytes().to_vec());
544 let s3_bucket = code
545 .and_then(|c| c.get("S3Bucket"))
546 .and_then(|v| v.as_str())
547 .map(|s| s.to_string());
548 let s3_key = code
549 .and_then(|c| c.get("S3Key"))
550 .and_then(|v| v.as_str())
551 .map(|s| s.to_string());
552 let image_uri = if package_type == "Image" {
556 code.and_then(|c| c.get("ImageUri"))
557 .and_then(|v| v.as_str())
558 .map(|s| s.to_string())
559 } else {
560 None
561 };
562 if package_type == "Image" && image_uri.is_none() {
563 return Err("Code.ImageUri is required when PackageType is Image".to_string());
564 }
565
566 let layers: Vec<String> = props
567 .get("Layers")
568 .and_then(|v| v.as_array())
569 .map(|arr| {
570 arr.iter()
571 .filter_map(|v| v.as_str().map(String::from))
572 .collect()
573 })
574 .unwrap_or_default();
575
576 let tracing_mode = props
577 .get("TracingConfig")
578 .and_then(|v| v.get("Mode"))
579 .and_then(|v| v.as_str())
580 .map(String::from);
581 let kms_key_arn = props
582 .get("KmsKeyArn")
583 .and_then(|v| v.as_str())
584 .map(String::from);
585 let ephemeral_storage_size = props
586 .get("EphemeralStorage")
587 .and_then(|v| v.get("Size"))
588 .and_then(|v| v.as_i64());
589 let vpc_config = props.get("VpcConfig").filter(|v| v.is_object()).cloned();
590 let snap_start = props.get("SnapStart").filter(|v| v.is_object()).cloned();
591 let dead_letter_config_arn = props
592 .get("DeadLetterConfig")
593 .and_then(|v| v.get("TargetArn"))
594 .and_then(|v| v.as_str())
595 .map(String::from);
596 let file_system_configs = props
597 .get("FileSystemConfigs")
598 .and_then(|v| v.as_array())
599 .cloned()
600 .unwrap_or_default();
601 let logging_config = props
602 .get("LoggingConfig")
603 .filter(|v| v.is_object())
604 .cloned();
605
606 Ok(LambdaFunctionProps {
607 runtime,
608 role,
609 handler,
610 description,
611 timeout,
612 memory_size,
613 package_type,
614 tags,
615 environment,
616 architectures,
617 code_zip,
618 s3_bucket,
619 s3_key,
620 image_uri,
621 layers,
622 tracing_mode,
623 kms_key_arn,
624 ephemeral_storage_size,
625 vpc_config,
626 snap_start,
627 dead_letter_config_arn,
628 file_system_configs,
629 logging_config,
630 })
631}
632
633struct LambdaEventSourceMappingProps {
638 event_source_arn: String,
639 batch_size: i64,
640 enabled: bool,
641 starting_position: Option<String>,
642 starting_position_timestamp: Option<f64>,
643 parallelization_factor: Option<i64>,
644 maximum_batching_window_in_seconds: Option<i64>,
645 function_response_types: Vec<String>,
646 filter_patterns: Vec<String>,
647 kms_key_arn: Option<String>,
648 metrics_config: Option<serde_json::Value>,
649 destination_config: Option<serde_json::Value>,
650 maximum_retry_attempts: Option<i64>,
651 maximum_record_age_in_seconds: Option<i64>,
652 bisect_batch_on_function_error: Option<bool>,
653 tumbling_window_in_seconds: Option<i64>,
654 topics: Vec<String>,
655 queues: Vec<String>,
656}
657
658fn parse_lambda_event_source_mapping_props(
662 props: &serde_json::Value,
663) -> Result<LambdaEventSourceMappingProps, String> {
664 let event_source_arn = props
665 .get("EventSourceArn")
666 .and_then(|v| v.as_str())
667 .unwrap_or_default()
668 .to_string();
669 let batch_size = props
670 .get("BatchSize")
671 .and_then(|v| v.as_i64())
672 .unwrap_or(10);
673 let enabled = props
674 .get("Enabled")
675 .and_then(|v| v.as_bool())
676 .unwrap_or(true);
677 let starting_position = props
678 .get("StartingPosition")
679 .and_then(|v| v.as_str())
680 .map(|s| s.to_string());
681 let starting_position_timestamp = props
682 .get("StartingPositionTimestamp")
683 .and_then(|v| v.as_f64());
684 let parallelization_factor = props.get("ParallelizationFactor").and_then(|v| v.as_i64());
685 let maximum_batching_window_in_seconds = props
686 .get("MaximumBatchingWindowInSeconds")
687 .and_then(|v| v.as_i64());
688 let function_response_types: Vec<String> = props
689 .get("FunctionResponseTypes")
690 .and_then(|v| v.as_array())
691 .map(|arr| {
692 arr.iter()
693 .filter_map(|v| v.as_str().map(|s| s.to_string()))
694 .collect()
695 })
696 .unwrap_or_default();
697 let filter_patterns: Vec<String> = props
698 .get("FilterCriteria")
699 .and_then(|v| v.get("Filters"))
700 .and_then(|v| v.as_array())
701 .map(|arr| {
702 arr.iter()
703 .filter_map(|f| {
704 f.get("Pattern")
705 .and_then(|p| p.as_str())
706 .map(|s| s.to_string())
707 })
708 .collect()
709 })
710 .unwrap_or_default();
711 let kms_key_arn = props
712 .get("KmsKeyArn")
713 .and_then(|v| v.as_str())
714 .map(|s| s.to_string());
715 let metrics_config = props
716 .get("MetricsConfig")
717 .filter(|v| v.is_object())
718 .cloned();
719 let destination_config = props
720 .get("DestinationConfig")
721 .filter(|v| v.is_object())
722 .cloned();
723 let maximum_retry_attempts = props.get("MaximumRetryAttempts").and_then(|v| v.as_i64());
724 let maximum_record_age_in_seconds = props
725 .get("MaximumRecordAgeInSeconds")
726 .and_then(|v| v.as_i64());
727 let bisect_batch_on_function_error = props
728 .get("BisectBatchOnFunctionError")
729 .and_then(|v| v.as_bool());
730 let tumbling_window_in_seconds = props
731 .get("TumblingWindowInSeconds")
732 .and_then(|v| v.as_i64());
733 let topics: Vec<String> = props
734 .get("Topics")
735 .and_then(|v| v.as_array())
736 .map(|arr| {
737 arr.iter()
738 .filter_map(|v| v.as_str().map(|s| s.to_string()))
739 .collect()
740 })
741 .unwrap_or_default();
742 let queues: Vec<String> = props
743 .get("Queues")
744 .and_then(|v| v.as_array())
745 .map(|arr| {
746 arr.iter()
747 .filter_map(|v| v.as_str().map(|s| s.to_string()))
748 .collect()
749 })
750 .unwrap_or_default();
751
752 Ok(LambdaEventSourceMappingProps {
753 event_source_arn,
754 batch_size,
755 enabled,
756 starting_position,
757 starting_position_timestamp,
758 parallelization_factor,
759 maximum_batching_window_in_seconds,
760 function_response_types,
761 filter_patterns,
762 kms_key_arn,
763 metrics_config,
764 destination_config,
765 maximum_retry_attempts,
766 maximum_record_age_in_seconds,
767 bisect_batch_on_function_error,
768 tumbling_window_in_seconds,
769 topics,
770 queues,
771 })
772}
773
774fn sha256_b64(bytes: &[u8]) -> String {
777 use sha2::Digest;
778 let hash = sha2::Sha256::digest(bytes);
779 base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash)
780}
781
782fn layer_code_size(
787 accounts: &fakecloud_core::multi_account::MultiAccountState<fakecloud_lambda::LambdaState>,
788 arn: &str,
789) -> i64 {
790 let Some(rest) = arn.strip_prefix("arn:aws:lambda:") else {
792 return 0;
793 };
794 let mut parts = rest.split(':');
795 let _region = parts.next();
796 let Some(account) = parts.next() else {
797 return 0;
798 };
799 if parts.next() != Some("layer") {
800 return 0;
801 }
802 let Some(name) = parts.next() else {
803 return 0;
804 };
805 let Some(ver_str) = parts.next() else {
806 return 0;
807 };
808 let Ok(ver) = ver_str.parse::<i64>() else {
809 return 0;
810 };
811 accounts
812 .get(account)
813 .and_then(|s| s.layers.get(name))
814 .and_then(|l| l.versions.iter().find(|v| v.version == ver))
815 .map(|v| v.code_size)
816 .unwrap_or(0)
817}
818
819pub struct ProvisionResult {
822 pub physical_id: String,
823 pub attributes: BTreeMap<String, String>,
824}
825
826impl ProvisionResult {
827 pub fn new(physical_id: impl Into<String>) -> Self {
828 Self {
829 physical_id: physical_id.into(),
830 attributes: BTreeMap::new(),
831 }
832 }
833
834 pub fn with(mut self, key: &str, value: impl Into<String>) -> Self {
835 self.attributes.insert(key.to_string(), value.into());
836 self
837 }
838
839 pub fn merge_attributes(mut self, other: BTreeMap<String, String>) -> Self {
844 for (k, v) in other {
845 self.attributes.insert(k, v);
846 }
847 self
848 }
849}
850
851fn policy_document_string(props: &serde_json::Value) -> Result<String, String> {
855 match props.get("PolicyDocument") {
856 Some(serde_json::Value::String(s)) => Ok(s.clone()),
857 Some(other) => Ok(other.to_string()),
858 None => Err("PolicyDocument is required".to_string()),
859 }
860}
861
862pub struct ResourceProvisioner {
864 pub sqs_state: SharedSqsState,
865 pub sns_state: SharedSnsState,
866 pub ssm_state: SharedSsmState,
867 pub iam_state: SharedIamState,
868 pub s3_state: SharedS3State,
869 pub eventbridge_state: SharedEventBridgeState,
870 pub dynamodb_state: SharedDynamoDbState,
871 pub logs_state: SharedLogsState,
872 pub lambda_state: SharedLambdaState,
873 pub secretsmanager_state: SharedSecretsManagerState,
874 pub kinesis_state: SharedKinesisState,
875 pub kms_state: SharedKmsState,
876 pub ecr_state: SharedEcrState,
877 pub cloudwatch_state: SharedCloudWatchState,
878 pub elbv2_state: SharedElbv2State,
879 pub organizations_state: SharedOrganizationsState,
880 pub cognito_state: SharedCognitoState,
881 pub rds_state: SharedRdsState,
882 pub ec2_state: fakecloud_ec2::SharedEc2State,
883 pub autoscaling_state: fakecloud_autoscaling::SharedAutoScalingState,
884 pub ecs_state: SharedEcsState,
885 pub acm_state: SharedAcmState,
886 pub elasticache_state: SharedElastiCacheState,
887 pub route53_state: SharedRoute53State,
888 pub cloudfront_state: SharedCloudFrontState,
889 pub stepfunctions_state: SharedStepFunctionsState,
890 pub wafv2_state: SharedWafv2State,
891 pub apigateway_state: SharedApiGatewayState,
892 pub apigatewayv2_state: SharedApiGatewayV2State,
893 pub ses_state: SharedSesState,
894 pub app_autoscaling_state: AppasState,
895 pub athena_state: SharedAthenaState,
896 pub firehose_state: fakecloud_firehose::SharedFirehoseState,
897 pub glue_state: fakecloud_glue::SharedGlueState,
898 pub cloudformation_state: SharedCloudFormationState,
899 pub delivery: Arc<DeliveryBus>,
900 pub lambda_runtime: Option<Arc<fakecloud_lambda::runtime::ContainerRuntime>>,
904 pub s3_store: Arc<dyn S3Store>,
909 pub account_id: String,
910 pub region: String,
911 pub stack_id: String,
912}
913
914mod acm;
915mod apigw;
916mod apigwv2;
917mod athena;
918mod autoscaling;
919mod cloudformation;
920mod cloudwatch;
921mod cognito;
922mod dynamodb;
923mod ec2;
924mod ecr;
925mod ecs;
926mod eventbridge;
927mod firehose;
928mod glue;
929mod iam;
930mod kinesis;
931mod kms;
932mod lambda;
933mod logs;
934mod rds;
935mod route;
936mod s3;
937mod secrets;
938mod ses;
939mod sns;
940mod sqs;
941mod ssm;
942mod stepfunctions;
943mod wafv2;
944
945impl ResourceProvisioner {
946 pub fn create_resource(&self, resource: &ResourceDefinition) -> Result<StackResource, String> {
948 let result = match resource.resource_type.as_str() {
949 "AWS::SQS::Queue" => self.create_sqs_queue(resource),
950 "AWS::SQS::QueuePolicy" => self.create_sqs_queue_policy(resource),
951 "AWS::SNS::Topic" => self.create_sns_topic(resource),
952 "AWS::SNS::TopicPolicy" => self.create_sns_topic_policy(resource),
953 "AWS::SNS::Subscription" => self.create_sns_subscription(resource),
954 "AWS::SSM::Parameter" => self.create_ssm_parameter(resource),
955 "AWS::IAM::Role" => self.create_iam_role(resource),
956 "AWS::IAM::Policy" => self.create_iam_policy(resource),
957 "AWS::IAM::User" => self.create_iam_user(resource),
958 "AWS::IAM::Group" => self.create_iam_group(resource),
959 "AWS::IAM::ManagedPolicy" => self.create_iam_managed_policy(resource),
960 "AWS::IAM::UserToGroupAddition" => self.create_iam_user_to_group_addition(resource),
961 "AWS::IAM::AccessKey" => self.create_iam_access_key(resource),
962 "AWS::IAM::InstanceProfile" => self.create_iam_instance_profile(resource),
963 "AWS::IAM::OIDCProvider" => self.create_iam_oidc_provider(resource),
964 "AWS::IAM::SAMLProvider" => self.create_iam_saml_provider(resource),
965 "AWS::IAM::ServiceLinkedRole" => self.create_iam_service_linked_role(resource),
966 "AWS::IAM::VirtualMFADevice" => self.create_iam_virtual_mfa_device(resource),
967 "AWS::S3::Bucket" => self.create_s3_bucket(resource),
968 "AWS::S3::BucketPolicy" => self.create_s3_bucket_policy(resource),
969 "AWS::Events::Rule" => self.create_eventbridge_rule(resource),
970 "AWS::Events::Connection" => self.create_eventbridge_connection(resource),
971 "AWS::Events::ApiDestination" => self.create_eventbridge_api_destination(resource),
972 "AWS::Events::Archive" => self.create_eventbridge_archive(resource),
973 "AWS::Events::EventBus" => self.create_eventbridge_event_bus(resource),
974 "AWS::Events::EventBusPolicy" => self.create_eventbridge_event_bus_policy(resource),
975 "AWS::Events::Endpoint" => self.create_eventbridge_endpoint(resource),
976 "AWS::DynamoDB::Table" => self.create_dynamodb_table(resource),
977 "AWS::Logs::LogGroup" => self.create_log_group(resource),
978 "AWS::Logs::LogStream" => self.create_log_stream(resource),
979 "AWS::Logs::MetricFilter" => self.create_metric_filter(resource),
980 "AWS::Logs::SubscriptionFilter" => self.create_subscription_filter(resource),
981 "AWS::Logs::Destination" => self.create_logs_destination(resource),
982 "AWS::Logs::ResourcePolicy" => self.create_logs_resource_policy(resource),
983 "AWS::Logs::QueryDefinition" => self.create_logs_query_definition(resource),
984 "AWS::Logs::Delivery" => self.create_logs_delivery(resource),
985 "AWS::Logs::DeliveryDestination" => self.create_logs_delivery_destination(resource),
986 "AWS::Logs::DeliverySource" => self.create_logs_delivery_source(resource),
987 "AWS::Lambda::Function" => self.create_lambda_function(resource),
988 "AWS::Lambda::Permission" => self.create_lambda_permission(resource),
989 "AWS::Lambda::EventSourceMapping" => self.create_lambda_event_source_mapping(resource),
990 "AWS::Lambda::LayerVersion" => self.create_lambda_layer_version(resource),
991 "AWS::Lambda::Url" => self.create_lambda_url(resource),
992 "AWS::Lambda::Alias" => self.create_lambda_alias(resource),
993 "AWS::Lambda::Version" => self.create_lambda_version(resource),
994 "AWS::SecretsManager::Secret" => self.create_secrets_manager_secret(resource),
995 "AWS::Kinesis::Stream" => self.create_kinesis_stream(resource),
996 "AWS::Kinesis::StreamConsumer" => self.create_kinesis_stream_consumer(resource),
997 "AWS::KMS::Key" => self.create_kms_key(resource),
998 "AWS::KMS::Alias" => self.create_kms_alias(resource),
999 "AWS::KMS::ReplicaKey" => self.create_kms_replica_key(resource),
1000 "AWS::ECR::Repository" => self.create_ecr_repository(resource),
1001 "AWS::ECR::RepositoryPolicy" => self.create_ecr_repository_policy(resource),
1002 "AWS::ECR::LifecyclePolicy" => self.create_ecr_lifecycle_policy(resource),
1003 "AWS::ECR::RegistryPolicy" => self.create_ecr_registry_policy(resource),
1004 "AWS::ECR::ReplicationConfiguration" => {
1005 self.create_ecr_replication_configuration(resource)
1006 }
1007 "AWS::ECR::RegistryScanningConfiguration" => {
1008 self.create_ecr_registry_scanning_configuration(resource)
1009 }
1010 "AWS::ECR::PullThroughCacheRule" => self.create_ecr_pull_through_cache_rule(resource),
1011 "AWS::CloudWatch::Alarm" => self.create_cloudwatch_alarm(resource),
1012 "AWS::CloudWatch::Dashboard" => self.create_cloudwatch_dashboard(resource),
1013 "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1014 self.create_elbv2_load_balancer(resource)
1015 }
1016 "AWS::ElasticLoadBalancingV2::TargetGroup" => self.create_elbv2_target_group(resource),
1017 "AWS::ElasticLoadBalancingV2::Listener" => self.create_elbv2_listener(resource),
1018 "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1019 self.create_elbv2_listener_rule(resource)
1020 }
1021 "AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
1022 self.create_elbv2_listener_certificate(resource)
1023 }
1024 "AWS::ElasticLoadBalancingV2::TrustStore" => self.create_elbv2_trust_store(resource),
1025 "AWS::Organizations::Organization" => self.create_organization(resource),
1026 "AWS::Organizations::OrganizationalUnit" => self.create_organization_unit(resource),
1027 "AWS::Organizations::Account" => self.create_organization_account(resource),
1028 "AWS::Organizations::Policy" => self.create_organization_policy(resource),
1029 "AWS::Organizations::ResourcePolicy" => {
1030 self.create_organization_resource_policy(resource)
1031 }
1032 "AWS::Cognito::UserPool" => self.create_cognito_user_pool(resource),
1033 "AWS::Cognito::UserPoolClient" => self.create_cognito_user_pool_client(resource),
1034 "AWS::Cognito::UserPoolDomain" => self.create_cognito_user_pool_domain(resource),
1035 "AWS::Cognito::IdentityPool" => self.create_cognito_identity_pool(resource),
1036 "AWS::Cognito::IdentityPoolRoleAttachment" => {
1037 self.create_cognito_identity_pool_role_attachment(resource)
1038 }
1039 "AWS::RDS::DBSubnetGroup" => self.create_rds_subnet_group(resource),
1040 "AWS::RDS::DBParameterGroup" => self.create_rds_parameter_group(resource),
1041 "AWS::RDS::DBClusterParameterGroup" => {
1042 self.create_rds_cluster_parameter_group(resource)
1043 }
1044 "AWS::RDS::OptionGroup" => self.create_rds_option_group(resource),
1045 "AWS::RDS::EventSubscription" => self.create_rds_event_subscription(resource),
1046 "AWS::RDS::DBSecurityGroup" => self.create_rds_security_group(resource),
1047 "AWS::RDS::DBProxy" => self.create_rds_db_proxy(resource),
1048 "AWS::RDS::DBInstance" => self.create_rds_db_instance(resource),
1049 "AWS::RDS::DBCluster" => self.create_rds_db_cluster(resource),
1050 "AWS::AutoScaling::LaunchConfiguration" => {
1051 self.create_autoscaling_launch_configuration(resource)
1052 }
1053 "AWS::AutoScaling::AutoScalingGroup" => self.create_autoscaling_group(resource),
1054 "AWS::EC2::VPC" => self.create_ec2_vpc(resource),
1055 "AWS::EC2::Subnet" => self.create_ec2_subnet(resource),
1056 "AWS::EC2::SecurityGroup" => self.create_ec2_security_group(resource),
1057 "AWS::EC2::InternetGateway" => self.create_ec2_internet_gateway(resource),
1058 "AWS::EC2::RouteTable" => self.create_ec2_route_table(resource),
1059 "AWS::ECS::Cluster" => self.create_ecs_cluster(resource),
1060 "AWS::ECS::TaskDefinition" => self.create_ecs_task_definition(resource),
1061 "AWS::ECS::Service" => self.create_ecs_service(resource),
1062 "AWS::ECS::CapacityProvider" => self.create_ecs_capacity_provider(resource),
1063 "AWS::CertificateManager::Certificate" => self.create_acm_certificate(resource),
1064 "AWS::CertificateManager::Account" => self.create_acm_account(resource),
1065 "AWS::ElastiCache::ParameterGroup" => self.create_ec_parameter_group(resource),
1066 "AWS::ElastiCache::SubnetGroup" => self.create_ec_subnet_group(resource),
1067 "AWS::ElastiCache::SecurityGroup" => self.create_ec_security_group(resource),
1068 "AWS::ElastiCache::User" => self.create_ec_user(resource),
1069 "AWS::ElastiCache::UserGroup" => self.create_ec_user_group(resource),
1070 "AWS::ElastiCache::CacheCluster" => self.create_ec_cache_cluster(resource),
1071 "AWS::ElastiCache::ReplicationGroup" => self.create_ec_replication_group(resource),
1072 "AWS::Route53::HostedZone" => self.create_route53_hosted_zone(resource),
1073 "AWS::Route53::RecordSet" => self.create_route53_record_set(resource),
1074 "AWS::Route53::HealthCheck" => self.create_route53_health_check(resource),
1075 "AWS::Route53::DNSSEC" => self.create_route53_dnssec(resource),
1076 "AWS::Route53::KeySigningKey" => self.create_route53_key_signing_key(resource),
1077 "AWS::CloudFront::CloudFrontOriginAccessIdentity" => {
1078 self.create_cf_origin_access_identity(resource)
1079 }
1080 "AWS::CloudFront::Distribution" => self.create_cf_distribution(resource),
1081 "AWS::CloudFront::OriginAccessControl" => {
1082 self.create_cf_origin_access_control(resource)
1083 }
1084 "AWS::CloudFront::PublicKey" => self.create_cf_public_key(resource),
1085 "AWS::CloudFront::KeyGroup" => self.create_cf_key_group(resource),
1086 "AWS::CloudFront::Function" => self.create_cf_function(resource),
1087 "AWS::CloudFront::CachePolicy" => self.create_cf_cache_policy(resource),
1088 "AWS::CloudFront::OriginRequestPolicy" => {
1089 self.create_cf_origin_request_policy(resource)
1090 }
1091 "AWS::CloudFront::ResponseHeadersPolicy" => {
1092 self.create_cf_response_headers_policy(resource)
1093 }
1094 "AWS::StepFunctions::StateMachine" => self.create_sfn_state_machine(resource),
1095 "AWS::StepFunctions::Activity" => self.create_sfn_activity(resource),
1096 "AWS::StepFunctions::StateMachineVersion" => self.create_sfn_version(resource),
1097 "AWS::StepFunctions::StateMachineAlias" => self.create_sfn_alias(resource),
1098 "AWS::WAFv2::WebACL" => self.create_wafv2_web_acl(resource),
1099 "AWS::WAFv2::IPSet" => self.create_wafv2_ip_set(resource),
1100 "AWS::WAFv2::RegexPatternSet" => self.create_wafv2_regex_pattern_set(resource),
1101 "AWS::WAFv2::RuleGroup" => self.create_wafv2_rule_group(resource),
1102 "AWS::WAFv2::LoggingConfiguration" => self.create_wafv2_logging_configuration(resource),
1103 "AWS::WAFv2::WebACLAssociation" => self.create_wafv2_web_acl_association(resource),
1104 "AWS::ApiGateway::RestApi" => self.create_apigw_rest_api(resource),
1105 "AWS::ApiGateway::Resource" => self.create_apigw_resource(resource),
1106 "AWS::ApiGateway::Method" => self.create_apigw_method(resource),
1107 "AWS::ApiGateway::Deployment" => self.create_apigw_deployment(resource),
1108 "AWS::ApiGateway::Stage" => self.create_apigw_stage(resource),
1109 "AWS::ApiGateway::Authorizer" => self.create_apigw_authorizer(resource),
1110 "AWS::ApiGateway::RequestValidator" => self.create_apigw_request_validator(resource),
1111 "AWS::ApiGateway::Model" => self.create_apigw_model(resource),
1112 "AWS::ApiGateway::GatewayResponse" => self.create_apigw_gateway_response(resource),
1113 "AWS::ApiGateway::UsagePlan" => self.create_apigw_usage_plan(resource),
1114 "AWS::ApiGateway::ApiKey" => self.create_apigw_api_key(resource),
1115 "AWS::ApiGateway::UsagePlanKey" => self.create_apigw_usage_plan_key(resource),
1116 "AWS::ApiGateway::DomainName" => self.create_apigw_domain_name(resource),
1117 "AWS::ApiGateway::BasePathMapping" => self.create_apigw_base_path_mapping(resource),
1118 "AWS::ApiGatewayV2::Api" => self.create_apigwv2_api(resource),
1119 "AWS::ApiGatewayV2::Route" => self.create_apigwv2_route(resource),
1120 "AWS::ApiGatewayV2::Integration" => self.create_apigwv2_integration(resource),
1121 "AWS::ApiGatewayV2::IntegrationResponse" => {
1122 self.create_apigwv2_integration_response(resource)
1123 }
1124 "AWS::ApiGatewayV2::RouteResponse" => self.create_apigwv2_route_response(resource),
1125 "AWS::ApiGatewayV2::Stage" => self.create_apigwv2_stage(resource),
1126 "AWS::ApiGatewayV2::Deployment" => self.create_apigwv2_deployment(resource),
1127 "AWS::ApiGatewayV2::Authorizer" => self.create_apigwv2_authorizer(resource),
1128 "AWS::ApiGatewayV2::DomainName" => self.create_apigwv2_domain_name(resource),
1129 "AWS::ApiGatewayV2::ApiMapping" => self.create_apigwv2_api_mapping(resource),
1130 "AWS::ApiGatewayV2::VpcLink" => self.create_apigwv2_vpc_link(resource),
1131 "AWS::ApiGatewayV2::Model" => self.create_apigwv2_model(resource),
1132 "AWS::SES::ConfigurationSet" => self.create_ses_configuration_set(resource),
1133 "AWS::SES::ConfigurationSetEventDestination" => {
1134 self.create_ses_event_destination(resource)
1135 }
1136 "AWS::SES::EmailIdentity" => self.create_ses_email_identity(resource),
1137 "AWS::SES::Template" => self.create_ses_template(resource),
1138 "AWS::SES::ContactList" => self.create_ses_contact_list(resource),
1139 "AWS::SES::DedicatedIpPool" => self.create_ses_dedicated_ip_pool(resource),
1140 "AWS::SES::ReceiptRule" => self.create_ses_receipt_rule(resource),
1141 "AWS::SES::ReceiptRuleSet" => self.create_ses_receipt_rule_set(resource),
1142 "AWS::SES::ReceiptFilter" => self.create_ses_receipt_filter(resource),
1143 "AWS::SES::VdmAttributes" => self.create_ses_vdm_attributes(resource),
1144 "AWS::SecretsManager::RotationSchedule" => {
1145 self.create_secrets_manager_rotation_schedule(resource)
1146 }
1147 "AWS::SecretsManager::ResourcePolicy" => {
1148 self.create_secrets_manager_resource_policy(resource)
1149 }
1150 "AWS::SecretsManager::SecretTargetAttachment" => {
1151 self.create_secrets_manager_target_attachment(resource)
1152 }
1153 "AWS::ApplicationAutoScaling::ScalableTarget" => {
1154 self.create_application_autoscaling_scalable_target(resource)
1155 }
1156 "AWS::ApplicationAutoScaling::ScalingPolicy" => {
1157 self.create_application_autoscaling_scaling_policy(resource)
1158 }
1159 "AWS::Athena::DataCatalog" => self.create_athena_data_catalog(resource),
1160 "AWS::Athena::NamedQuery" => self.create_athena_named_query(resource),
1161 "AWS::Athena::WorkGroup" => self.create_athena_work_group(resource),
1162 "AWS::Athena::PreparedStatement" => self.create_athena_prepared_statement(resource),
1163 "AWS::KinesisFirehose::DeliveryStream" => {
1164 self.create_firehose_delivery_stream(resource)
1165 }
1166 "AWS::Glue::Database" => self.create_glue_database(resource),
1167 "AWS::CloudFormation::Stack" => self.create_cloudformation_stack(resource),
1168 "AWS::Glue::Table" => self.create_glue_table(resource),
1169 "AWS::Glue::Partition" => self.create_glue_partition(resource),
1170 t if t.starts_with("Custom::") || t == "AWS::CloudFormation::CustomResource" => self
1171 .create_custom_resource(resource)
1172 .map(ProvisionResult::new),
1173 other => {
1174 tracing::warn!(
1185 resource_type = %other,
1186 logical_id = %resource.logical_id,
1187 "CloudFormation: no provisioner for resource type; recording it as provisioned with no backing state"
1188 );
1189 Ok(ProvisionResult::new(resource.logical_id.clone()))
1190 }
1191 };
1192
1193 let is_custom = resource.resource_type.starts_with("Custom::")
1194 || resource.resource_type == "AWS::CloudFormation::CustomResource";
1195 let service_token = if is_custom {
1196 resource
1197 .properties
1198 .get("ServiceToken")
1199 .and_then(|v| v.as_str())
1200 .map(|s| s.to_string())
1201 } else {
1202 None
1203 };
1204
1205 result.map(|res| StackResource {
1206 logical_id: resource.logical_id.clone(),
1207 physical_id: res.physical_id,
1208 resource_type: resource.resource_type.clone(),
1209 status: "CREATE_COMPLETE".to_string(),
1210 service_token,
1211 attributes: res.attributes,
1212 })
1213 }
1214
1215 pub fn update_resource(
1222 &self,
1223 existing: &StackResource,
1224 new_def: &ResourceDefinition,
1225 ) -> Result<Option<StackResource>, String> {
1226 let result = match new_def.resource_type.as_str() {
1227 "AWS::Lambda::Function" => Some(self.update_lambda_function(existing, new_def)?),
1228 "AWS::Lambda::Permission" => Some(self.update_lambda_permission(existing, new_def)?),
1229 "AWS::Lambda::EventSourceMapping" => {
1230 Some(self.update_lambda_event_source_mapping(existing, new_def)?)
1231 }
1232 "AWS::Lambda::LayerVersion" => {
1233 Some(self.update_lambda_layer_version(existing, new_def)?)
1234 }
1235 "AWS::Lambda::Url" => Some(self.update_lambda_url(existing, new_def)?),
1236 "AWS::Lambda::Alias" => Some(self.update_lambda_alias(existing, new_def)?),
1237 "AWS::Lambda::Version" => Some(self.update_lambda_version(existing, new_def)?),
1238 "AWS::IAM::Role" => Some(self.update_iam_role(existing, new_def)?),
1239 "AWS::IAM::Policy" => Some(self.update_iam_policy(existing, new_def)?),
1240 "AWS::IAM::ManagedPolicy" => Some(self.update_iam_policy(existing, new_def)?),
1241 "AWS::ApiGateway::RestApi" => Some(self.update_apigw_rest_api(existing, new_def)?),
1242 "AWS::ApiGateway::Resource" => Some(self.update_apigw_resource(existing, new_def)?),
1243 "AWS::ApiGateway::Method" => Some(self.update_apigw_method(existing, new_def)?),
1244 "AWS::ApiGateway::Deployment" => Some(self.update_apigw_deployment(existing, new_def)?),
1245 "AWS::ApiGateway::Stage" => Some(self.update_apigw_stage(existing, new_def)?),
1246 "AWS::ApiGateway::Authorizer" => Some(self.update_apigw_authorizer(existing, new_def)?),
1247 "AWS::ApiGateway::RequestValidator" => {
1248 Some(self.update_apigw_request_validator(existing, new_def)?)
1249 }
1250 "AWS::ApiGateway::Model" => Some(self.update_apigw_model(existing, new_def)?),
1251 "AWS::ApiGateway::GatewayResponse" => {
1252 Some(self.update_apigw_gateway_response(existing, new_def)?)
1253 }
1254 "AWS::ApiGateway::UsagePlan" => Some(self.update_apigw_usage_plan(existing, new_def)?),
1255 "AWS::ApiGateway::ApiKey" => Some(self.update_apigw_api_key(existing, new_def)?),
1256 "AWS::ApiGateway::UsagePlanKey" => {
1257 Some(self.update_apigw_usage_plan_key(existing, new_def)?)
1258 }
1259 "AWS::ApiGateway::DomainName" => {
1260 Some(self.update_apigw_domain_name(existing, new_def)?)
1261 }
1262 "AWS::ApiGateway::BasePathMapping" => {
1263 Some(self.update_apigw_base_path_mapping(existing, new_def)?)
1264 }
1265 "AWS::ApiGatewayV2::Api" => Some(self.update_apigwv2_api(existing, new_def)?),
1266 "AWS::ApiGatewayV2::Route" => Some(self.update_apigwv2_route(existing, new_def)?),
1267 "AWS::ApiGatewayV2::Integration" => {
1268 Some(self.update_apigwv2_integration(existing, new_def)?)
1269 }
1270 "AWS::ApiGatewayV2::IntegrationResponse" => {
1271 Some(self.update_apigwv2_integration_response(existing, new_def)?)
1272 }
1273 "AWS::ApiGatewayV2::RouteResponse" => {
1274 Some(self.update_apigwv2_route_response(existing, new_def)?)
1275 }
1276 "AWS::ApiGatewayV2::Stage" => Some(self.update_apigwv2_stage(existing, new_def)?),
1277 "AWS::ApiGatewayV2::Deployment" => {
1278 Some(self.update_apigwv2_deployment(existing, new_def)?)
1279 }
1280 "AWS::ApiGatewayV2::Authorizer" => {
1281 Some(self.update_apigwv2_authorizer(existing, new_def)?)
1282 }
1283 "AWS::ApiGatewayV2::DomainName" => {
1284 Some(self.update_apigwv2_domain_name(existing, new_def)?)
1285 }
1286 "AWS::ApiGatewayV2::ApiMapping" => {
1287 Some(self.update_apigwv2_api_mapping(existing, new_def)?)
1288 }
1289 "AWS::ApiGatewayV2::VpcLink" => Some(self.update_apigwv2_vpc_link(existing, new_def)?),
1290 "AWS::ApiGatewayV2::Model" => Some(self.update_apigwv2_model(existing, new_def)?),
1291 "AWS::ECS::Cluster" => Some(self.update_ecs_cluster(existing, new_def)?),
1292 "AWS::ECS::Service" => Some(self.update_ecs_service(existing, new_def)?),
1293 "AWS::ECS::TaskDefinition" => Some(self.update_ecs_task_definition(existing, new_def)?),
1294 "AWS::ECS::CapacityProvider" => {
1295 Some(self.update_ecs_capacity_provider(existing, new_def)?)
1296 }
1297 "AWS::ECR::Repository" => Some(self.update_ecr_repository(existing, new_def)?),
1298 "AWS::ECR::RepositoryPolicy" => {
1299 Some(self.update_ecr_repository_policy(existing, new_def)?)
1300 }
1301 "AWS::ECR::LifecyclePolicy" => {
1302 Some(self.update_ecr_lifecycle_policy(existing, new_def)?)
1303 }
1304 "AWS::ECR::RegistryPolicy" => Some(self.update_ecr_registry_policy(existing, new_def)?),
1305 "AWS::ECR::ReplicationConfiguration" => {
1306 Some(self.update_ecr_replication_configuration(existing, new_def)?)
1307 }
1308 "AWS::ECR::RegistryScanningConfiguration" => {
1309 Some(self.update_ecr_registry_scanning_configuration(existing, new_def)?)
1310 }
1311 "AWS::ECR::PullThroughCacheRule" => {
1312 Some(self.update_ecr_pull_through_cache_rule(existing, new_def)?)
1313 }
1314 "AWS::KMS::Key" => Some(self.update_kms_key(existing, new_def)?),
1315 "AWS::KMS::ReplicaKey" => Some(self.update_kms_replica_key(existing, new_def)?),
1316 "AWS::KMS::Alias" => Some(self.update_kms_alias(existing, new_def)?),
1317 "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1318 Some(self.update_elbv2_load_balancer(existing, new_def)?)
1319 }
1320 "AWS::ElasticLoadBalancingV2::TargetGroup" => {
1321 Some(self.update_elbv2_target_group(existing, new_def)?)
1322 }
1323 "AWS::ElasticLoadBalancingV2::Listener" => {
1324 Some(self.update_elbv2_listener(existing, new_def)?)
1325 }
1326 "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1327 Some(self.update_elbv2_listener_rule(existing, new_def)?)
1328 }
1329 "AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
1330 Some(self.update_elbv2_listener_certificate(existing, new_def)?)
1331 }
1332 "AWS::ElasticLoadBalancingV2::TrustStore" => {
1333 Some(self.update_elbv2_trust_store(existing, new_def)?)
1334 }
1335 "AWS::CloudWatch::Alarm" => Some(self.update_cloudwatch_alarm(existing, new_def)?),
1336 "AWS::CloudWatch::Dashboard" => {
1337 Some(self.update_cloudwatch_dashboard(existing, new_def)?)
1338 }
1339 "AWS::StepFunctions::StateMachine" => {
1340 Some(self.update_sfn_state_machine(existing, new_def)?)
1341 }
1342 "AWS::SQS::Queue" => Some(self.update_sqs_queue(existing, new_def)?),
1343 "AWS::SQS::QueuePolicy" => Some(self.update_sqs_queue_policy(existing, new_def)?),
1344 "AWS::SNS::Topic" => Some(self.update_sns_topic(existing, new_def)?),
1345 "AWS::SNS::TopicPolicy" => Some(self.update_sns_topic_policy(existing, new_def)?),
1346 "AWS::S3::BucketPolicy" => Some(self.update_s3_bucket_policy(existing, new_def)?),
1347 _ => None,
1348 };
1349
1350 Ok(result.map(|res| StackResource {
1351 logical_id: existing.logical_id.clone(),
1352 physical_id: res.physical_id,
1353 resource_type: existing.resource_type.clone(),
1354 status: "UPDATE_COMPLETE".to_string(),
1355 service_token: existing.service_token.clone(),
1356 attributes: res.attributes,
1357 }))
1358 }
1359
1360 pub fn get_att(&self, resource: &StackResource, attribute: &str) -> Option<String> {
1371 if let Some(v) = resource.attributes.get(attribute) {
1374 return Some(v.clone());
1375 }
1376 match resource.resource_type.as_str() {
1379 "AWS::S3::Bucket" => self.get_att_s3_bucket(&resource.physical_id, attribute),
1380 "AWS::Lambda::Function" => {
1381 self.get_att_lambda_function(&resource.physical_id, attribute)
1382 }
1383 "AWS::IAM::Role" => self.get_att_iam_role(&resource.physical_id, attribute),
1384 "AWS::SQS::Queue" => self.get_att_sqs_queue(&resource.physical_id, attribute),
1385 "AWS::SNS::Topic" => self.get_att_sns_topic(&resource.physical_id, attribute),
1386 "AWS::DynamoDB::Table" => self.get_att_dynamodb_table(&resource.physical_id, attribute),
1387 "AWS::KMS::Key" => self.get_att_kms_key(&resource.physical_id, attribute),
1388 "AWS::SecretsManager::Secret" => {
1389 self.get_att_secrets_manager_secret(&resource.physical_id, attribute)
1390 }
1391 "AWS::CloudFront::Distribution" => {
1392 self.get_att_cf_distribution(&resource.physical_id, attribute)
1393 }
1394 "AWS::ECS::Cluster" => self.get_att_ecs_cluster(&resource.physical_id, attribute),
1395 "AWS::ECS::Service" => self.get_att_ecs_service(&resource.physical_id, attribute),
1396 "AWS::EC2::VPC"
1397 | "AWS::EC2::Subnet"
1398 | "AWS::EC2::SecurityGroup"
1399 | "AWS::EC2::InternetGateway"
1400 | "AWS::EC2::RouteTable" => self.get_att_ec2(resource, attribute),
1401 "AWS::ECS::CapacityProvider" => {
1402 self.get_att_ecs_capacity_provider(&resource.physical_id, attribute)
1403 }
1404 "AWS::ECR::Repository" => self.get_att_ecr_repository(&resource.physical_id, attribute),
1405 "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1406 self.get_att_elbv2_load_balancer(&resource.physical_id, attribute)
1407 }
1408 "AWS::ElasticLoadBalancingV2::TargetGroup" => {
1409 self.get_att_elbv2_target_group(&resource.physical_id, attribute)
1410 }
1411 "AWS::ElasticLoadBalancingV2::Listener" => {
1412 self.get_att_elbv2_listener(&resource.physical_id, attribute)
1413 }
1414 "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1415 self.get_att_elbv2_listener_rule(&resource.physical_id, attribute)
1416 }
1417 "AWS::ElasticLoadBalancingV2::TrustStore" => {
1418 self.get_att_elbv2_trust_store(&resource.physical_id, attribute)
1419 }
1420 "AWS::WAFv2::WebACL" => self.get_att_wafv2_web_acl(&resource.physical_id, attribute),
1421 "AWS::WAFv2::IPSet" => self.get_att_wafv2_ip_set(&resource.physical_id, attribute),
1422 "AWS::WAFv2::RegexPatternSet" => {
1423 self.get_att_wafv2_regex_pattern_set(&resource.physical_id, attribute)
1424 }
1425 "AWS::WAFv2::RuleGroup" => {
1426 self.get_att_wafv2_rule_group(&resource.physical_id, attribute)
1427 }
1428 "AWS::SES::ConfigurationSet" => {
1429 self.get_att_ses_configuration_set(&resource.physical_id, attribute)
1430 }
1431 "AWS::SES::EmailIdentity" => {
1432 self.get_att_ses_email_identity(&resource.physical_id, attribute)
1433 }
1434 "AWS::SES::Template" => self.get_att_ses_template(&resource.physical_id, attribute),
1435 "AWS::SES::ContactList" => {
1436 self.get_att_ses_contact_list(&resource.physical_id, attribute)
1437 }
1438 "AWS::SES::DedicatedIpPool" => {
1439 self.get_att_ses_dedicated_ip_pool(&resource.physical_id, attribute)
1440 }
1441 "AWS::SES::ReceiptRuleSet" => {
1442 self.get_att_ses_receipt_rule_set(&resource.physical_id, attribute)
1443 }
1444 "AWS::Athena::DataCatalog" => {
1445 self.get_att_athena_data_catalog(&resource.physical_id, attribute)
1446 }
1447 "AWS::Athena::NamedQuery" => {
1448 self.get_att_athena_named_query(&resource.physical_id, attribute)
1449 }
1450 "AWS::Athena::WorkGroup" => {
1451 self.get_att_athena_work_group(&resource.physical_id, attribute)
1452 }
1453 "AWS::Athena::PreparedStatement" => {
1454 self.get_att_athena_prepared_statement(&resource.physical_id, attribute)
1455 }
1456 "AWS::CloudFormation::Stack" => {
1457 self.get_att_cloudformation_stack(&resource.physical_id, attribute)
1458 }
1459 _ => None,
1460 }
1461 }
1462
1463 fn get_att_cf_distribution(&self, physical_id: &str, attribute: &str) -> Option<String> {
1464 let accounts = self.cloudfront_state.read();
1467 let state = accounts.get("000000000000")?;
1468 let dist = state.distributions.get(physical_id)?;
1469 match attribute {
1470 "DomainName" => Some(dist.domain_name.clone()),
1471 "Id" => Some(dist.id.clone()),
1472 _ => None,
1473 }
1474 }
1475
1476 pub fn delete_resource(&self, resource: &StackResource) -> Result<(), String> {
1478 match resource.resource_type.as_str() {
1479 "AWS::SQS::Queue" => self.delete_sqs_queue(&resource.physical_id),
1480 "AWS::SQS::QueuePolicy" => self.delete_sqs_queue_policy(&resource.physical_id),
1481 "AWS::SNS::Topic" => self.delete_sns_topic(&resource.physical_id),
1482 "AWS::SNS::TopicPolicy" => self.delete_sns_topic_policy(&resource.physical_id),
1483 "AWS::SNS::Subscription" => self.delete_sns_subscription(&resource.physical_id),
1484 "AWS::SSM::Parameter" => self.delete_ssm_parameter(&resource.physical_id),
1485 "AWS::IAM::Role" => self.delete_iam_role(&resource.physical_id),
1486 "AWS::IAM::Policy" => self.delete_iam_policy(&resource.physical_id),
1487 "AWS::IAM::User" => self.delete_iam_user(&resource.physical_id),
1488 "AWS::IAM::Group" => self.delete_iam_group(&resource.physical_id),
1489 "AWS::IAM::ManagedPolicy" => self.delete_iam_managed_policy(&resource.physical_id),
1490 "AWS::IAM::UserToGroupAddition" => {
1491 self.delete_iam_user_to_group_addition(&resource.physical_id)
1492 }
1493 "AWS::IAM::AccessKey" => self.delete_iam_access_key(&resource.physical_id),
1494 "AWS::IAM::InstanceProfile" => self.delete_iam_instance_profile(&resource.physical_id),
1495 "AWS::IAM::OIDCProvider" => self.delete_iam_oidc_provider(&resource.physical_id),
1496 "AWS::IAM::SAMLProvider" => self.delete_iam_saml_provider(&resource.physical_id),
1497 "AWS::IAM::ServiceLinkedRole" => {
1498 self.delete_iam_service_linked_role(&resource.physical_id)
1499 }
1500 "AWS::IAM::VirtualMFADevice" => {
1501 self.delete_iam_virtual_mfa_device(&resource.physical_id)
1502 }
1503 "AWS::S3::Bucket" => self.delete_s3_bucket(&resource.physical_id),
1504 "AWS::S3::BucketPolicy" => self.delete_s3_bucket_policy(&resource.physical_id),
1505 "AWS::Events::Rule" => self.delete_eventbridge_rule(&resource.physical_id),
1506 "AWS::Events::Connection" => self.delete_eventbridge_connection(&resource.physical_id),
1507 "AWS::Events::EventBus" => self.delete_eventbridge_event_bus(&resource.physical_id),
1508 "AWS::Events::EventBusPolicy" => {
1509 self.delete_eventbridge_event_bus_policy(&resource.physical_id)
1510 }
1511 "AWS::Events::Endpoint" => self.delete_eventbridge_endpoint(&resource.physical_id),
1512 "AWS::Events::ApiDestination" => {
1513 self.delete_eventbridge_api_destination(&resource.physical_id)
1514 }
1515 "AWS::Events::Archive" => self.delete_eventbridge_archive(&resource.physical_id),
1516 "AWS::DynamoDB::Table" => self.delete_dynamodb_table(&resource.physical_id),
1517 "AWS::Logs::LogGroup" => self.delete_log_group(&resource.physical_id),
1518 "AWS::Logs::LogStream" => self.delete_log_stream(&resource.physical_id),
1519 "AWS::Logs::MetricFilter" => self.delete_metric_filter(&resource.physical_id),
1520 "AWS::Logs::SubscriptionFilter" => {
1521 self.delete_subscription_filter(&resource.physical_id)
1522 }
1523 "AWS::Logs::Destination" => self.delete_logs_destination(&resource.physical_id),
1524 "AWS::Logs::ResourcePolicy" => self.delete_logs_resource_policy(&resource.physical_id),
1525 "AWS::Logs::QueryDefinition" => {
1526 self.delete_logs_query_definition(&resource.physical_id)
1527 }
1528 "AWS::Logs::Delivery" => self.delete_logs_delivery(&resource.physical_id),
1529 "AWS::Logs::DeliveryDestination" => {
1530 self.delete_logs_delivery_destination(&resource.physical_id)
1531 }
1532 "AWS::Logs::DeliverySource" => self.delete_logs_delivery_source(&resource.physical_id),
1533 "AWS::Lambda::Function" => self.delete_lambda_function(&resource.physical_id),
1534 "AWS::Lambda::Permission" => self.delete_lambda_permission(&resource.physical_id),
1535 "AWS::Lambda::EventSourceMapping" => {
1536 self.delete_lambda_event_source_mapping(&resource.physical_id)
1537 }
1538 "AWS::Lambda::LayerVersion" => self.delete_lambda_layer_version(&resource.physical_id),
1539 "AWS::Lambda::Url" => self.delete_lambda_url(&resource.physical_id),
1540 "AWS::Lambda::Alias" => self.delete_lambda_alias(&resource.physical_id),
1541 "AWS::Lambda::Version" => self.delete_lambda_version(&resource.physical_id),
1542 "AWS::SecretsManager::Secret" => {
1543 self.delete_secrets_manager_secret(&resource.physical_id)
1544 }
1545 "AWS::Kinesis::Stream" => self.delete_kinesis_stream(&resource.physical_id),
1546 "AWS::Kinesis::StreamConsumer" => {
1547 self.delete_kinesis_stream_consumer(&resource.physical_id)
1548 }
1549 "AWS::KMS::Key" => self.delete_kms_key(&resource.physical_id),
1550 "AWS::KMS::ReplicaKey" => self.delete_kms_replica_key(&resource.physical_id),
1551 "AWS::KMS::Alias" => self.delete_kms_alias(&resource.physical_id),
1552 "AWS::ECR::Repository" => self.delete_ecr_repository(&resource.physical_id),
1553 "AWS::ECR::RepositoryPolicy" => {
1554 self.delete_ecr_repository_policy(&resource.physical_id)
1555 }
1556 "AWS::ECR::LifecyclePolicy" => self.delete_ecr_lifecycle_policy(&resource.physical_id),
1557 "AWS::ECR::RegistryPolicy" => self.delete_ecr_registry_policy(),
1558 "AWS::ECR::ReplicationConfiguration" => self.delete_ecr_replication_configuration(),
1559 "AWS::ECR::RegistryScanningConfiguration" => {
1560 self.delete_ecr_registry_scanning_configuration()
1561 }
1562 "AWS::ECR::PullThroughCacheRule" => {
1563 self.delete_ecr_pull_through_cache_rule(&resource.physical_id)
1564 }
1565 "AWS::CloudWatch::Alarm" => self.delete_cloudwatch_alarm(&resource.physical_id),
1566 "AWS::CloudWatch::Dashboard" => self.delete_cloudwatch_dashboard(&resource.physical_id),
1567 "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1568 self.delete_elbv2_load_balancer(&resource.physical_id)
1569 }
1570 "AWS::ElasticLoadBalancingV2::TargetGroup" => {
1571 self.delete_elbv2_target_group(&resource.physical_id)
1572 }
1573 "AWS::ElasticLoadBalancingV2::Listener" => {
1574 self.delete_elbv2_listener(&resource.physical_id)
1575 }
1576 "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1577 self.delete_elbv2_listener_rule(&resource.physical_id)
1578 }
1579 "AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
1580 self.delete_elbv2_listener_certificate(&resource.physical_id)
1581 }
1582 "AWS::ElasticLoadBalancingV2::TrustStore" => {
1583 self.delete_elbv2_trust_store(&resource.physical_id)
1584 }
1585 "AWS::Organizations::Organization" => self.delete_organization(&resource.physical_id),
1586 "AWS::Organizations::OrganizationalUnit" => {
1587 self.delete_organization_unit(&resource.physical_id)
1588 }
1589 "AWS::Organizations::Account" => {
1590 self.delete_organization_account(&resource.physical_id)
1591 }
1592 "AWS::Organizations::Policy" => self.delete_organization_policy(&resource.physical_id),
1593 "AWS::Organizations::ResourcePolicy" => {
1594 self.delete_organization_resource_policy(&resource.physical_id)
1595 }
1596 "AWS::Cognito::UserPool" => self.delete_cognito_user_pool(&resource.physical_id),
1597 "AWS::Cognito::UserPoolClient" => {
1598 self.delete_cognito_user_pool_client(&resource.physical_id)
1599 }
1600 "AWS::Cognito::UserPoolDomain" => {
1601 self.delete_cognito_user_pool_domain(&resource.physical_id)
1602 }
1603 "AWS::Cognito::IdentityPool" => {
1604 self.delete_cognito_identity_pool(&resource.physical_id)
1605 }
1606 "AWS::Cognito::IdentityPoolRoleAttachment" => {
1607 self.delete_cognito_identity_pool_role_attachment(&resource.physical_id)
1608 }
1609 "AWS::RDS::DBSubnetGroup" => self.delete_rds_subnet_group(&resource.physical_id),
1610 "AWS::RDS::DBParameterGroup" => self.delete_rds_parameter_group(&resource.physical_id),
1611 "AWS::RDS::DBClusterParameterGroup" => {
1612 self.delete_rds_cluster_parameter_group(&resource.physical_id)
1613 }
1614 "AWS::RDS::OptionGroup" => self.delete_rds_option_group(&resource.physical_id),
1615 "AWS::RDS::EventSubscription" => {
1616 self.delete_rds_event_subscription(&resource.physical_id)
1617 }
1618 "AWS::RDS::DBSecurityGroup" => self.delete_rds_security_group(&resource.physical_id),
1619 "AWS::RDS::DBProxy" => self.delete_rds_db_proxy(&resource.physical_id),
1620 "AWS::RDS::DBInstance" => self.delete_rds_db_instance(&resource.physical_id),
1621 "AWS::RDS::DBCluster" => self.delete_rds_db_cluster(&resource.physical_id),
1622 "AWS::EC2::VPC"
1623 | "AWS::EC2::Subnet"
1624 | "AWS::EC2::SecurityGroup"
1625 | "AWS::EC2::InternetGateway"
1626 | "AWS::EC2::RouteTable" => {
1627 self.delete_ec2_resource(&resource.resource_type, &resource.physical_id)
1628 }
1629 "AWS::AutoScaling::LaunchConfiguration" | "AWS::AutoScaling::AutoScalingGroup" => {
1630 self.delete_autoscaling(&resource.resource_type, &resource.physical_id);
1631 Ok(())
1632 }
1633 "AWS::ECS::Cluster" => self.delete_ecs_cluster(&resource.physical_id),
1634 "AWS::ECS::TaskDefinition" => self.delete_ecs_task_definition(&resource.physical_id),
1635 "AWS::ECS::Service" => self.delete_ecs_service(&resource.physical_id),
1636 "AWS::ECS::CapacityProvider" => {
1637 self.delete_ecs_capacity_provider(&resource.physical_id)
1638 }
1639 "AWS::CertificateManager::Certificate" => {
1640 self.delete_acm_certificate(&resource.physical_id)
1641 }
1642 "AWS::CertificateManager::Account" => self.delete_acm_account(),
1643 "AWS::ElastiCache::ParameterGroup" => {
1644 self.delete_ec_parameter_group(&resource.physical_id)
1645 }
1646 "AWS::ElastiCache::SubnetGroup" => self.delete_ec_subnet_group(&resource.physical_id),
1647 "AWS::ElastiCache::SecurityGroup" => {
1648 self.delete_ec_security_group(&resource.physical_id)
1649 }
1650 "AWS::ElastiCache::User" => self.delete_ec_user(&resource.physical_id),
1651 "AWS::ElastiCache::UserGroup" => self.delete_ec_user_group(&resource.physical_id),
1652 "AWS::ElastiCache::CacheCluster" => self.delete_ec_cache_cluster(&resource.physical_id),
1653 "AWS::ElastiCache::ReplicationGroup" => {
1654 self.delete_ec_replication_group(&resource.physical_id)
1655 }
1656 "AWS::Route53::HostedZone" => self.delete_route53_hosted_zone(&resource.physical_id),
1657 "AWS::Route53::RecordSet" => {
1658 self.delete_route53_record_set(&resource.physical_id, &resource.attributes)
1659 }
1660 "AWS::Route53::HealthCheck" => self.delete_route53_health_check(&resource.physical_id),
1661 "AWS::Route53::DNSSEC" => self.delete_route53_dnssec(&resource.physical_id),
1662 "AWS::Route53::KeySigningKey" => {
1663 self.delete_route53_key_signing_key(&resource.physical_id)
1664 }
1665 "AWS::CloudFront::CloudFrontOriginAccessIdentity" => {
1666 self.delete_cf_origin_access_identity(&resource.physical_id)
1667 }
1668 "AWS::CloudFront::Distribution" => self.delete_cf_distribution(&resource.physical_id),
1669 "AWS::CloudFront::OriginAccessControl" => {
1670 self.delete_cf_origin_access_control(&resource.physical_id)
1671 }
1672 "AWS::CloudFront::PublicKey" => self.delete_cf_public_key(&resource.physical_id),
1673 "AWS::CloudFront::KeyGroup" => self.delete_cf_key_group(&resource.physical_id),
1674 "AWS::CloudFront::Function" => self.delete_cf_function(&resource.physical_id),
1675 "AWS::CloudFront::CachePolicy" => self.delete_cf_cache_policy(&resource.physical_id),
1676 "AWS::CloudFront::OriginRequestPolicy" => {
1677 self.delete_cf_origin_request_policy(&resource.physical_id)
1678 }
1679 "AWS::CloudFront::ResponseHeadersPolicy" => {
1680 self.delete_cf_response_headers_policy(&resource.physical_id)
1681 }
1682 "AWS::StepFunctions::StateMachine" => {
1683 self.delete_sfn_state_machine(&resource.physical_id)
1684 }
1685 "AWS::StepFunctions::Activity" => self.delete_sfn_activity(&resource.physical_id),
1686 "AWS::StepFunctions::StateMachineVersion" => {
1687 self.delete_sfn_version(&resource.physical_id)
1688 }
1689 "AWS::StepFunctions::StateMachineAlias" => self.delete_sfn_alias(&resource.physical_id),
1690 "AWS::WAFv2::WebACL" => self.delete_wafv2_web_acl(&resource.physical_id),
1691 "AWS::WAFv2::IPSet" => self.delete_wafv2_ip_set(&resource.physical_id),
1692 "AWS::WAFv2::RegexPatternSet" => {
1693 self.delete_wafv2_regex_pattern_set(&resource.physical_id)
1694 }
1695 "AWS::WAFv2::RuleGroup" => self.delete_wafv2_rule_group(&resource.physical_id),
1696 "AWS::WAFv2::LoggingConfiguration" => {
1697 self.delete_wafv2_logging_configuration(&resource.physical_id)
1698 }
1699 "AWS::WAFv2::WebACLAssociation" => {
1700 self.delete_wafv2_web_acl_association(&resource.physical_id)
1701 }
1702 "AWS::ApiGateway::RestApi" => self.delete_apigw_rest_api(&resource.physical_id),
1703 "AWS::ApiGateway::Resource" => {
1704 self.delete_apigw_resource(&resource.physical_id, &resource.attributes)
1705 }
1706 "AWS::ApiGateway::Method" => self.delete_apigw_method(&resource.physical_id),
1707 "AWS::ApiGateway::Deployment" => {
1708 self.delete_apigw_deployment(&resource.physical_id, &resource.attributes)
1709 }
1710 "AWS::ApiGateway::Stage" => {
1711 self.delete_apigw_stage(&resource.physical_id, &resource.attributes)
1712 }
1713 "AWS::ApiGateway::Authorizer" => {
1714 self.delete_apigw_authorizer(&resource.physical_id, &resource.attributes)
1715 }
1716 "AWS::ApiGateway::RequestValidator" => {
1717 self.delete_apigw_request_validator(&resource.physical_id, &resource.attributes)
1718 }
1719 "AWS::ApiGateway::Model" => {
1720 self.delete_apigw_model(&resource.physical_id, &resource.attributes)
1721 }
1722 "AWS::ApiGateway::GatewayResponse" => {
1723 self.delete_apigw_gateway_response(&resource.physical_id, &resource.attributes)
1724 }
1725 "AWS::ApiGateway::UsagePlan" => self.delete_apigw_usage_plan(&resource.physical_id),
1726 "AWS::ApiGateway::ApiKey" => self.delete_apigw_api_key(&resource.physical_id),
1727 "AWS::ApiGateway::UsagePlanKey" => {
1728 self.delete_apigw_usage_plan_key(&resource.physical_id, &resource.attributes)
1729 }
1730 "AWS::ApiGateway::DomainName" => self.delete_apigw_domain_name(&resource.physical_id),
1731 "AWS::ApiGateway::BasePathMapping" => {
1732 self.delete_apigw_base_path_mapping(&resource.physical_id, &resource.attributes)
1733 }
1734 "AWS::ApiGatewayV2::Api" => self.delete_apigwv2_api(&resource.physical_id),
1735 "AWS::ApiGatewayV2::Route" => {
1736 self.delete_apigwv2_route(&resource.physical_id, &resource.attributes)
1737 }
1738 "AWS::ApiGatewayV2::Integration" => {
1739 self.delete_apigwv2_integration(&resource.physical_id, &resource.attributes)
1740 }
1741 "AWS::ApiGatewayV2::IntegrationResponse" => self
1742 .delete_apigwv2_integration_response(&resource.physical_id, &resource.attributes),
1743 "AWS::ApiGatewayV2::RouteResponse" => {
1744 self.delete_apigwv2_route_response(&resource.physical_id, &resource.attributes)
1745 }
1746 "AWS::ApiGatewayV2::Stage" => {
1747 self.delete_apigwv2_stage(&resource.physical_id, &resource.attributes)
1748 }
1749 "AWS::ApiGatewayV2::Deployment" => {
1750 self.delete_apigwv2_deployment(&resource.physical_id, &resource.attributes)
1751 }
1752 "AWS::ApiGatewayV2::Authorizer" => {
1753 self.delete_apigwv2_authorizer(&resource.physical_id, &resource.attributes)
1754 }
1755 "AWS::ApiGatewayV2::DomainName" => {
1756 self.delete_apigwv2_domain_name(&resource.physical_id)
1757 }
1758 "AWS::ApiGatewayV2::ApiMapping" => {
1759 self.delete_apigwv2_api_mapping(&resource.physical_id, &resource.attributes)
1760 }
1761 "AWS::ApiGatewayV2::VpcLink" => self.delete_apigwv2_vpc_link(&resource.physical_id),
1762 "AWS::ApiGatewayV2::Model" => {
1763 self.delete_apigwv2_model(&resource.physical_id, &resource.attributes)
1764 }
1765 "AWS::SES::ConfigurationSet" => {
1766 self.delete_ses_configuration_set(&resource.physical_id)
1767 }
1768 "AWS::SES::ConfigurationSetEventDestination" => {
1769 self.delete_ses_event_destination(&resource.physical_id, &resource.attributes)
1770 }
1771 "AWS::SES::EmailIdentity" => self.delete_ses_email_identity(&resource.physical_id),
1772 "AWS::SES::Template" => self.delete_ses_template(&resource.physical_id),
1773 "AWS::SES::ContactList" => self.delete_ses_contact_list(&resource.physical_id),
1774 "AWS::SES::DedicatedIpPool" => self.delete_ses_dedicated_ip_pool(&resource.physical_id),
1775 "AWS::SES::ReceiptRule" => {
1776 self.delete_ses_receipt_rule(&resource.physical_id, &resource.attributes)
1777 }
1778 "AWS::SES::ReceiptRuleSet" => self.delete_ses_receipt_rule_set(&resource.physical_id),
1779 "AWS::SES::ReceiptFilter" => self.delete_ses_receipt_filter(&resource.physical_id),
1780 "AWS::SES::VdmAttributes" => Ok(()),
1781 "AWS::SecretsManager::RotationSchedule" => {
1782 self.delete_secrets_manager_rotation_schedule(&resource.physical_id)
1783 }
1784 "AWS::SecretsManager::ResourcePolicy" => {
1785 self.delete_secrets_manager_resource_policy(&resource.physical_id)
1786 }
1787 "AWS::SecretsManager::SecretTargetAttachment" => Ok(()),
1788 "AWS::ApplicationAutoScaling::ScalableTarget" => self
1789 .delete_application_autoscaling_scalable_target(
1790 &resource.physical_id,
1791 &resource.attributes,
1792 ),
1793 "AWS::ApplicationAutoScaling::ScalingPolicy" => self
1794 .delete_application_autoscaling_scaling_policy(
1795 &resource.physical_id,
1796 &resource.attributes,
1797 ),
1798 "AWS::Athena::DataCatalog" => self.delete_athena_data_catalog(&resource.physical_id),
1799 "AWS::Athena::NamedQuery" => self.delete_athena_named_query(&resource.physical_id),
1800 "AWS::Athena::WorkGroup" => self.delete_athena_work_group(&resource.physical_id),
1801 "AWS::Athena::PreparedStatement" => {
1802 self.delete_athena_prepared_statement(&resource.physical_id, &resource.attributes)
1803 }
1804 "AWS::KinesisFirehose::DeliveryStream" => {
1805 self.delete_firehose_delivery_stream(&resource.physical_id)
1806 }
1807 "AWS::Glue::Database" => self.delete_glue_database(&resource.physical_id),
1808 "AWS::CloudFormation::Stack" => self.delete_cloudformation_stack(&resource.physical_id),
1809 "AWS::Glue::Table" => self.delete_glue_table(&resource.physical_id),
1810 "AWS::Glue::Partition" => {
1811 self.delete_glue_partition(&resource.physical_id, &resource.attributes)
1812 }
1813 t if t.starts_with("Custom::") || t == "AWS::CloudFormation::CustomResource" => {
1814 self.delete_custom_resource(resource)
1815 }
1816 _ => Ok(()),
1820 }
1821 }
1822
1823 fn create_log_group(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
1826 let props = &resource.properties;
1827 let log_group_name = props
1828 .get("LogGroupName")
1829 .and_then(|v| v.as_str())
1830 .unwrap_or(&resource.logical_id);
1831
1832 let retention_in_days = props
1833 .get("RetentionInDays")
1834 .and_then(|v| v.as_i64())
1835 .map(|v| v as i32);
1836
1837 let mut logs_accounts = self.logs_state.write();
1838 let state = logs_accounts.get_or_create(&self.account_id);
1839 let arn = format!(
1840 "arn:aws:logs:{}:{}:log-group:{}:*",
1841 state.region, state.account_id, log_group_name
1842 );
1843
1844 let log_group = fakecloud_logs::LogGroup {
1845 name: log_group_name.to_string(),
1846 arn: arn.clone(),
1847 creation_time: Utc::now().timestamp_millis(),
1848 retention_in_days,
1849 kms_key_id: None,
1850 stored_bytes: 0,
1851 log_streams: std::collections::BTreeMap::new(),
1852 tags: std::collections::BTreeMap::new(),
1853 subscription_filters: Vec::new(),
1854 data_protection_policy: None,
1855 index_policies: Vec::new(),
1856 transformer: None,
1857 deletion_protection: false,
1858 log_group_class: Some("STANDARD".to_string()),
1859 };
1860
1861 state
1862 .log_groups
1863 .insert(log_group_name.to_string(), log_group);
1864 Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
1865 }
1866
1867 fn read_s3_object_bytes(&self, bucket: &str, key: &str) -> Result<Vec<u8>, String> {
1873 let mut accounts = self.s3_state.write();
1874 let state = accounts.get_or_create(&self.account_id);
1875 let body_ref = {
1876 let b = state
1877 .buckets
1878 .get(bucket)
1879 .ok_or_else(|| format!("S3 bucket {bucket} does not exist"))?;
1880 let object = b
1881 .objects
1882 .get(key)
1883 .ok_or_else(|| format!("S3 object s3://{bucket}/{key} does not exist"))?;
1884 object.body.clone()
1885 };
1886 state
1889 .read_body(&body_ref)
1890 .map(|b| b.to_vec())
1891 .map_err(|e| format!("S3 read failed: {e}"))
1892 }
1893
1894 fn read_s3_object_version_bytes(
1900 &self,
1901 bucket: &str,
1902 key: &str,
1903 version_id: &str,
1904 ) -> Result<Vec<u8>, String> {
1905 let mut accounts = self.s3_state.write();
1906 let state = accounts.get_or_create(&self.account_id);
1907 let body_ref = {
1908 let b = state
1909 .buckets
1910 .get(bucket)
1911 .ok_or_else(|| format!("S3 bucket {bucket} does not exist"))?;
1912 let from_current = b
1913 .objects
1914 .get(key)
1915 .filter(|o| o.version_id.as_deref() == Some(version_id))
1916 .map(|o| o.body.clone());
1917 from_current
1918 .or_else(|| {
1919 b.object_versions.get(key).and_then(|versions| {
1920 versions
1921 .iter()
1922 .find(|o| o.version_id.as_deref() == Some(version_id))
1923 .map(|o| o.body.clone())
1924 })
1925 })
1926 .ok_or_else(|| {
1927 format!("S3 object s3://{bucket}/{key} version {version_id} does not exist")
1928 })?
1929 };
1930 state
1931 .read_body(&body_ref)
1932 .map(|b| b.to_vec())
1933 .map_err(|e| format!("S3 read failed: {e}"))
1934 }
1935
1936 fn append_lambda_permission_statement(
1942 &self,
1943 function_name: &str,
1944 statement_id: &str,
1945 props: &serde_json::Value,
1946 ) -> Result<String, String> {
1947 let action = props
1948 .get("Action")
1949 .and_then(|v| v.as_str())
1950 .ok_or_else(|| "Action is required".to_string())?
1951 .to_string();
1952 let principal = props
1953 .get("Principal")
1954 .and_then(|v| v.as_str())
1955 .ok_or_else(|| "Principal is required".to_string())?
1956 .to_string();
1957 let source_arn = props
1958 .get("SourceArn")
1959 .and_then(|v| v.as_str())
1960 .map(|s| s.to_string());
1961 let source_account = props
1962 .get("SourceAccount")
1963 .and_then(|v| v.as_str())
1964 .map(|s| s.to_string());
1965 let event_source_token = props
1966 .get("EventSourceToken")
1967 .and_then(|v| v.as_str())
1968 .map(|s| s.to_string());
1969 let function_url_auth_type = props
1970 .get("FunctionUrlAuthType")
1971 .and_then(|v| v.as_str())
1972 .map(|s| s.to_string());
1973 let principal_org_id = props
1974 .get("PrincipalOrgID")
1975 .and_then(|v| v.as_str())
1976 .map(|s| s.to_string());
1977
1978 let mut accounts = self.lambda_state.write();
1979 let state = accounts.get_or_create(&self.account_id);
1980 let func = state.functions.get_mut(function_name).ok_or_else(|| {
1981 format!(
1982 "Function {function_name} does not exist yet — retry once it has been provisioned"
1983 )
1984 })?;
1985
1986 let mut doc: serde_json::Value = func
1987 .policy
1988 .as_deref()
1989 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
1990 .filter(|v| v.is_object())
1991 .unwrap_or_else(|| serde_json::json!({"Version": "2012-10-17", "Statement": []}));
1992 if !doc.get("Statement").map(|s| s.is_array()).unwrap_or(false) {
1993 doc["Statement"] = serde_json::json!([]);
1994 }
1995 let principal_value =
1996 if principal.ends_with(".amazonaws.com") || principal.contains(".amazon") {
1997 serde_json::json!({ "Service": principal })
1998 } else {
1999 serde_json::json!({ "AWS": principal })
2000 };
2001 let mut arn_like = serde_json::Map::new();
2002 let mut string_equals = serde_json::Map::new();
2003 if let Some(src) = source_arn {
2004 arn_like.insert("AWS:SourceArn".to_string(), serde_json::Value::String(src));
2005 }
2006 if let Some(acct) = source_account {
2007 string_equals.insert(
2008 "AWS:SourceAccount".to_string(),
2009 serde_json::Value::String(acct),
2010 );
2011 }
2012 if let Some(token) = event_source_token {
2013 string_equals.insert(
2014 "lambda:EventSourceToken".to_string(),
2015 serde_json::Value::String(token),
2016 );
2017 }
2018 if let Some(auth) = function_url_auth_type {
2019 string_equals.insert(
2020 "lambda:FunctionUrlAuthType".to_string(),
2021 serde_json::Value::String(auth),
2022 );
2023 }
2024 if let Some(org) = principal_org_id {
2025 string_equals.insert(
2026 "aws:PrincipalOrgID".to_string(),
2027 serde_json::Value::String(org),
2028 );
2029 }
2030 let mut conditions = serde_json::Map::new();
2031 if !arn_like.is_empty() {
2032 conditions.insert("ArnLike".to_string(), serde_json::Value::Object(arn_like));
2033 }
2034 if !string_equals.is_empty() {
2035 conditions.insert(
2036 "StringEquals".to_string(),
2037 serde_json::Value::Object(string_equals),
2038 );
2039 }
2040
2041 let mut statement = serde_json::Map::new();
2042 statement.insert(
2043 "Sid".to_string(),
2044 serde_json::Value::String(statement_id.to_string()),
2045 );
2046 statement.insert(
2047 "Effect".to_string(),
2048 serde_json::Value::String("Allow".to_string()),
2049 );
2050 statement.insert("Principal".to_string(), principal_value);
2051 statement.insert("Action".to_string(), serde_json::Value::String(action));
2052 statement.insert(
2053 "Resource".to_string(),
2054 serde_json::Value::String(func.function_arn.clone()),
2055 );
2056 if !conditions.is_empty() {
2057 statement.insert(
2058 "Condition".to_string(),
2059 serde_json::Value::Object(conditions),
2060 );
2061 }
2062 doc["Statement"]
2063 .as_array_mut()
2064 .unwrap()
2065 .push(serde_json::Value::Object(statement));
2066 func.policy = Some(doc.to_string());
2067 Ok(func.function_arn.clone())
2068 }
2069
2070 fn create_elbv2_load_balancer(
2073 &self,
2074 resource: &ResourceDefinition,
2075 ) -> Result<ProvisionResult, String> {
2076 let props = &resource.properties;
2077 let name = props
2078 .get("Name")
2079 .and_then(|v| v.as_str())
2080 .unwrap_or(&resource.logical_id)
2081 .to_string();
2082 let scheme = props
2083 .get("Scheme")
2084 .and_then(|v| v.as_str())
2085 .unwrap_or("internet-facing")
2086 .to_string();
2087 let lb_type = props
2088 .get("Type")
2089 .and_then(|v| v.as_str())
2090 .unwrap_or("application")
2091 .to_string();
2092 let ip_address_type = props
2093 .get("IpAddressType")
2094 .and_then(|v| v.as_str())
2095 .unwrap_or("ipv4")
2096 .to_string();
2097 let security_groups: Vec<String> = props
2098 .get("SecurityGroups")
2099 .and_then(|v| v.as_array())
2100 .map(|arr| {
2101 arr.iter()
2102 .filter_map(|s| s.as_str().map(|s| s.to_string()))
2103 .collect()
2104 })
2105 .unwrap_or_default();
2106 let tags = parse_elb_tags(props.get("Tags"));
2107
2108 let mut accounts = self.elbv2_state.write();
2109 let state = accounts.get_or_create(&self.account_id);
2110 let lb_id = Uuid::new_v4().simple().to_string();
2111 let arn = format!(
2112 "arn:aws:elasticloadbalancing:{}:{}:loadbalancer/{}/{}/{}",
2113 self.region,
2114 self.account_id,
2115 if lb_type == "network" { "net" } else { "app" },
2116 name,
2117 &lb_id[..16]
2118 );
2119 let dns_name = format!(
2120 "{}-{}.{}.elb.{}.amazonaws.com",
2121 name,
2122 &lb_id[..16],
2123 self.region,
2124 self.region
2125 );
2126
2127 let mut availability_zones: Vec<fakecloud_elbv2::AvailabilityZone> = Vec::new();
2128 if let Some(arr) = props.get("Subnets").and_then(|v| v.as_array()) {
2129 for s in arr {
2130 if let Some(subnet_id) = s.as_str() {
2131 availability_zones.push(fakecloud_elbv2::AvailabilityZone {
2132 zone_name: format!("{}a", self.region),
2133 subnet_id: subnet_id.to_string(),
2134 outpost_id: None,
2135 load_balancer_addresses: Vec::new(),
2136 source_nat_ipv6_prefixes: Vec::new(),
2137 });
2138 }
2139 }
2140 }
2141
2142 state.load_balancers.insert(
2143 arn.clone(),
2144 LoadBalancer {
2145 arn: arn.clone(),
2146 name: name.clone(),
2147 dns_name: dns_name.clone(),
2148 canonical_hosted_zone_id: "Z2P70J7EXAMPLE".to_string(),
2149 created_time: Utc::now(),
2150 scheme,
2151 vpc_id: String::new(),
2152 state_code: "active".to_string(),
2153 state_reason: None,
2154 lb_type,
2155 availability_zones,
2156 security_groups,
2157 ip_address_type,
2158 customer_owned_ipv4_pool: None,
2159 enforce_security_group_inbound_rules_on_private_link_traffic: None,
2160 enable_prefix_for_ipv6_source_nat: None,
2161 ipv4_ipam_pool_id: None,
2162 tags,
2163 attributes: BTreeMap::new(),
2164 minimum_capacity_units: None,
2165 bound_port: None,
2166 },
2167 );
2168
2169 Ok(ProvisionResult::new(arn.clone())
2170 .with("LoadBalancerArn", arn)
2171 .with(
2172 "LoadBalancerFullName",
2173 format!("app/{name}/{}", &lb_id[..16]),
2174 )
2175 .with("LoadBalancerName", name)
2176 .with("DNSName", dns_name)
2177 .with("CanonicalHostedZoneID", "Z2P70J7EXAMPLE"))
2178 }
2179
2180 fn delete_elbv2_load_balancer(&self, physical_id: &str) -> Result<(), String> {
2181 let mut accounts = self.elbv2_state.write();
2182 let state = accounts.get_or_create(&self.account_id);
2183 state.load_balancers.remove(physical_id);
2184 let listeners: Vec<String> = state
2186 .listeners
2187 .iter()
2188 .filter(|(_, l)| l.load_balancer_arn == physical_id)
2189 .map(|(arn, _)| arn.clone())
2190 .collect();
2191 for arn in &listeners {
2192 state.listeners.remove(arn);
2193 let rules: Vec<String> = state
2194 .rules
2195 .iter()
2196 .filter(|(_, r)| r.listener_arn == *arn)
2197 .map(|(a, _)| a.clone())
2198 .collect();
2199 for r in rules {
2200 state.rules.remove(&r);
2201 }
2202 }
2203 for tg in state.target_groups.values_mut() {
2204 tg.load_balancer_arns.retain(|a| a != physical_id);
2205 }
2206 Ok(())
2207 }
2208
2209 fn create_elbv2_target_group(
2210 &self,
2211 resource: &ResourceDefinition,
2212 ) -> Result<ProvisionResult, String> {
2213 let props = &resource.properties;
2214 let name = props
2215 .get("Name")
2216 .and_then(|v| v.as_str())
2217 .unwrap_or(&resource.logical_id)
2218 .to_string();
2219 let protocol = props
2220 .get("Protocol")
2221 .and_then(|v| v.as_str())
2222 .map(|s| s.to_string());
2223 let port = props.get("Port").and_then(|v| v.as_i64()).map(|n| n as i32);
2224 let vpc_id = props
2225 .get("VpcId")
2226 .and_then(|v| v.as_str())
2227 .map(|s| s.to_string());
2228 let target_type = props
2229 .get("TargetType")
2230 .and_then(|v| v.as_str())
2231 .unwrap_or("instance")
2232 .to_string();
2233 let ip_address_type = props
2234 .get("IpAddressType")
2235 .and_then(|v| v.as_str())
2236 .unwrap_or("ipv4")
2237 .to_string();
2238 let protocol_version = props
2239 .get("ProtocolVersion")
2240 .and_then(|v| v.as_str())
2241 .map(|s| s.to_string());
2242 let tags = parse_elb_tags(props.get("Tags"));
2243
2244 let mut accounts = self.elbv2_state.write();
2245 let state = accounts.get_or_create(&self.account_id);
2246 let id = Uuid::new_v4().simple().to_string();
2247 let arn = format!(
2248 "arn:aws:elasticloadbalancing:{}:{}:targetgroup/{}/{}",
2249 self.region,
2250 self.account_id,
2251 name,
2252 &id[..16]
2253 );
2254
2255 state.target_groups.insert(
2256 arn.clone(),
2257 TargetGroup {
2258 arn: arn.clone(),
2259 name: name.clone(),
2260 protocol,
2261 port,
2262 vpc_id,
2263 target_type,
2264 ip_address_type,
2265 protocol_version,
2266 health_check_protocol: props
2267 .get("HealthCheckProtocol")
2268 .and_then(|v| v.as_str())
2269 .map(|s| s.to_string()),
2270 health_check_port: props
2271 .get("HealthCheckPort")
2272 .and_then(|v| v.as_str())
2273 .map(|s| s.to_string()),
2274 health_check_enabled: props
2275 .get("HealthCheckEnabled")
2276 .and_then(|v| v.as_bool())
2277 .unwrap_or(true),
2278 health_check_path: props
2279 .get("HealthCheckPath")
2280 .and_then(|v| v.as_str())
2281 .map(|s| s.to_string()),
2282 health_check_interval_seconds: props
2283 .get("HealthCheckIntervalSeconds")
2284 .and_then(|v| v.as_i64())
2285 .unwrap_or(30) as i32,
2286 health_check_timeout_seconds: props
2287 .get("HealthCheckTimeoutSeconds")
2288 .and_then(|v| v.as_i64())
2289 .unwrap_or(5) as i32,
2290 healthy_threshold_count: props
2291 .get("HealthyThresholdCount")
2292 .and_then(|v| v.as_i64())
2293 .unwrap_or(5) as i32,
2294 unhealthy_threshold_count: props
2295 .get("UnhealthyThresholdCount")
2296 .and_then(|v| v.as_i64())
2297 .unwrap_or(2) as i32,
2298 matcher_http_code: props
2299 .get("Matcher")
2300 .and_then(|v| v.get("HttpCode"))
2301 .and_then(|v| v.as_str())
2302 .map(|s| s.to_string()),
2303 matcher_grpc_code: props
2304 .get("Matcher")
2305 .and_then(|v| v.get("GrpcCode"))
2306 .and_then(|v| v.as_str())
2307 .map(|s| s.to_string()),
2308 load_balancer_arns: Vec::new(),
2309 targets: Vec::new(),
2310 tags,
2311 attributes: BTreeMap::new(),
2312 created_time: Utc::now(),
2313 },
2314 );
2315
2316 Ok(ProvisionResult::new(arn.clone())
2317 .with("TargetGroupArn", arn)
2318 .with("TargetGroupName", name)
2319 .with("TargetGroupFullName", format!("targetgroup/{}", &id[..16])))
2320 }
2321
2322 fn delete_elbv2_target_group(&self, physical_id: &str) -> Result<(), String> {
2323 let mut accounts = self.elbv2_state.write();
2324 let state = accounts.get_or_create(&self.account_id);
2325 state.target_groups.remove(physical_id);
2326 Ok(())
2327 }
2328
2329 fn create_elbv2_listener(
2330 &self,
2331 resource: &ResourceDefinition,
2332 ) -> Result<ProvisionResult, String> {
2333 let props = &resource.properties;
2334 let load_balancer_arn = props
2335 .get("LoadBalancerArn")
2336 .and_then(|v| v.as_str())
2337 .ok_or_else(|| "LoadBalancerArn is required".to_string())?
2338 .to_string();
2339 let port = props.get("Port").and_then(|v| v.as_i64()).map(|n| n as i32);
2340 let protocol = props
2341 .get("Protocol")
2342 .and_then(|v| v.as_str())
2343 .map(|s| s.to_string());
2344 let default_actions = parse_elb_actions(props.get("DefaultActions"));
2345
2346 let mut accounts = self.elbv2_state.write();
2347 let state = accounts.get_or_create(&self.account_id);
2348 if !state.load_balancers.contains_key(&load_balancer_arn) {
2349 return Err(format!(
2350 "LoadBalancer {load_balancer_arn} not yet provisioned"
2351 ));
2352 }
2353
2354 let lb_full = load_balancer_arn
2355 .rsplit("loadbalancer/")
2356 .next()
2357 .unwrap_or("")
2358 .to_string();
2359 let listener_id = Uuid::new_v4().simple().to_string();
2360 let arn = format!(
2361 "arn:aws:elasticloadbalancing:{}:{}:listener/{}/{}",
2362 self.region,
2363 self.account_id,
2364 lb_full,
2365 &listener_id[..16]
2366 );
2367
2368 for action in &default_actions {
2371 if let Some(tg_arn) = &action.target_group_arn {
2372 if let Some(tg) = state.target_groups.get_mut(tg_arn) {
2373 if !tg.load_balancer_arns.contains(&load_balancer_arn) {
2374 tg.load_balancer_arns.push(load_balancer_arn.clone());
2375 }
2376 }
2377 }
2378 if let Some(forward) = &action.forward {
2379 for tgt in &forward.target_groups {
2380 if let Some(tg) = state.target_groups.get_mut(&tgt.target_group_arn) {
2381 if !tg.load_balancer_arns.contains(&load_balancer_arn) {
2382 tg.load_balancer_arns.push(load_balancer_arn.clone());
2383 }
2384 }
2385 }
2386 }
2387 }
2388
2389 state.listeners.insert(
2390 arn.clone(),
2391 Listener {
2392 arn: arn.clone(),
2393 load_balancer_arn,
2394 port,
2395 protocol,
2396 certificates: Vec::new(),
2397 ssl_policy: props
2398 .get("SslPolicy")
2399 .and_then(|v| v.as_str())
2400 .map(|s| s.to_string()),
2401 default_actions,
2402 alpn_policy: Vec::new(),
2403 mutual_authentication: None,
2404 tags: parse_elb_tags(props.get("Tags")),
2405 attributes: BTreeMap::new(),
2406 },
2407 );
2408
2409 Ok(ProvisionResult::new(arn.clone()).with("ListenerArn", arn))
2410 }
2411
2412 fn delete_elbv2_listener(&self, physical_id: &str) -> Result<(), String> {
2413 let mut accounts = self.elbv2_state.write();
2414 let state = accounts.get_or_create(&self.account_id);
2415 state.listeners.remove(physical_id);
2416 let rules: Vec<String> = state
2417 .rules
2418 .iter()
2419 .filter(|(_, r)| r.listener_arn == physical_id)
2420 .map(|(arn, _)| arn.clone())
2421 .collect();
2422 for r in rules {
2423 state.rules.remove(&r);
2424 }
2425 Ok(())
2426 }
2427
2428 fn create_elbv2_listener_rule(
2429 &self,
2430 resource: &ResourceDefinition,
2431 ) -> Result<ProvisionResult, String> {
2432 let props = &resource.properties;
2433 let listener_arn = props
2434 .get("ListenerArn")
2435 .and_then(|v| v.as_str())
2436 .ok_or_else(|| "ListenerArn is required".to_string())?
2437 .to_string();
2438 let priority = props
2439 .get("Priority")
2440 .map(|v| {
2441 if let Some(s) = v.as_str() {
2442 s.to_string()
2443 } else if let Some(n) = v.as_i64() {
2444 n.to_string()
2445 } else {
2446 "1".to_string()
2447 }
2448 })
2449 .unwrap_or_else(|| "1".to_string());
2450 let actions = parse_elb_actions(props.get("Actions"));
2451 let conditions = parse_elb_rule_conditions(props.get("Conditions"));
2452
2453 let mut accounts = self.elbv2_state.write();
2454 let state = accounts.get_or_create(&self.account_id);
2455 if !state.listeners.contains_key(&listener_arn) {
2456 return Err(format!("Listener {listener_arn} not yet provisioned"));
2457 }
2458 let listener_full = listener_arn
2459 .rsplit("listener/")
2460 .next()
2461 .unwrap_or("")
2462 .to_string();
2463 let rule_id = Uuid::new_v4().simple().to_string();
2464 let arn = format!(
2465 "arn:aws:elasticloadbalancing:{}:{}:listener-rule/{}/{}",
2466 self.region,
2467 self.account_id,
2468 listener_full,
2469 &rule_id[..16]
2470 );
2471
2472 state.rules.insert(
2473 arn.clone(),
2474 ElbRule {
2475 arn: arn.clone(),
2476 listener_arn,
2477 priority,
2478 conditions,
2479 actions,
2480 is_default: false,
2481 tags: parse_elb_tags(props.get("Tags")),
2482 },
2483 );
2484
2485 Ok(ProvisionResult::new(arn.clone()).with("RuleArn", arn))
2486 }
2487
2488 fn delete_elbv2_listener_rule(&self, physical_id: &str) -> Result<(), String> {
2489 let mut accounts = self.elbv2_state.write();
2490 let state = accounts.get_or_create(&self.account_id);
2491 state.rules.remove(physical_id);
2492 Ok(())
2493 }
2494
2495 fn create_elbv2_listener_certificate(
2500 &self,
2501 resource: &ResourceDefinition,
2502 ) -> Result<ProvisionResult, String> {
2503 let props = &resource.properties;
2504 let listener_arn = props
2505 .get("ListenerArn")
2506 .and_then(|v| v.as_str())
2507 .ok_or_else(|| "ListenerArn is required".to_string())?
2508 .to_string();
2509 let certs: Vec<String> = props
2510 .get("Certificates")
2511 .and_then(|v| v.as_array())
2512 .map(|arr| {
2513 arr.iter()
2514 .filter_map(|c| c.get("CertificateArn").and_then(|v| v.as_str()))
2515 .map(|s| s.to_string())
2516 .collect()
2517 })
2518 .unwrap_or_default();
2519 if certs.is_empty() {
2520 return Err("Certificates must contain at least one CertificateArn".to_string());
2521 }
2522 let mut accounts = self.elbv2_state.write();
2523 let state = accounts.get_or_create(&self.account_id);
2524 let listener = state
2525 .listeners
2526 .get_mut(&listener_arn)
2527 .ok_or_else(|| format!("Listener {listener_arn} does not exist"))?;
2528 for arn in &certs {
2529 listener.certificates.retain(|c| &c.certificate_arn != arn);
2530 listener.certificates.push(fakecloud_elbv2::Certificate {
2531 certificate_arn: arn.clone(),
2532 is_default: false,
2533 });
2534 }
2535 Ok(ProvisionResult::new(format!(
2536 "{}#{}",
2537 listener_arn,
2538 certs.join(",")
2539 )))
2540 }
2541
2542 fn delete_elbv2_listener_certificate(&self, physical_id: &str) -> Result<(), String> {
2543 let (listener_arn, cert_list) = match physical_id.split_once('#') {
2544 Some(parts) => parts,
2545 None => return Ok(()),
2546 };
2547 let cert_arns: Vec<&str> = cert_list.split(',').collect();
2548 let mut accounts = self.elbv2_state.write();
2549 let state = accounts.get_or_create(&self.account_id);
2550 if let Some(listener) = state.listeners.get_mut(listener_arn) {
2551 listener
2552 .certificates
2553 .retain(|c| !cert_arns.iter().any(|a| *a == c.certificate_arn));
2554 }
2555 Ok(())
2556 }
2557
2558 fn create_elbv2_trust_store(
2560 &self,
2561 resource: &ResourceDefinition,
2562 ) -> Result<ProvisionResult, String> {
2563 let props = &resource.properties;
2564 let name = props
2565 .get("Name")
2566 .and_then(|v| v.as_str())
2567 .unwrap_or(&resource.logical_id)
2568 .to_string();
2569 let bucket = props
2570 .get("CaCertificatesBundleS3Bucket")
2571 .and_then(|v| v.as_str())
2572 .ok_or_else(|| "CaCertificatesBundleS3Bucket is required".to_string())?;
2573 let key = props
2574 .get("CaCertificatesBundleS3Key")
2575 .and_then(|v| v.as_str())
2576 .ok_or_else(|| "CaCertificatesBundleS3Key is required".to_string())?;
2577 let tags: Vec<fakecloud_elbv2::Tag> = props
2578 .get("Tags")
2579 .and_then(|v| v.as_array())
2580 .map(|arr| {
2581 arr.iter()
2582 .filter_map(|t| {
2583 let k = t.get("Key").and_then(|v| v.as_str())?;
2584 let val = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
2585 Some(fakecloud_elbv2::Tag {
2586 key: k.to_string(),
2587 value: val.to_string(),
2588 })
2589 })
2590 .collect()
2591 })
2592 .unwrap_or_default();
2593
2594 let mut accounts = self.elbv2_state.write();
2595 let state = accounts.get_or_create(&self.account_id);
2596 if state.trust_stores.values().any(|t| t.name == name) {
2597 return Err(format!("Trust store {name} already exists"));
2598 }
2599 let suffix: String = Uuid::new_v4()
2600 .simple()
2601 .to_string()
2602 .chars()
2603 .take(16)
2604 .collect();
2605 let arn = format!(
2606 "arn:aws:elasticloadbalancing:{}:{}:truststore/{}/{}",
2607 self.region, self.account_id, name, suffix
2608 );
2609 let ts = fakecloud_elbv2::TrustStore {
2610 arn: arn.clone(),
2611 name: name.clone(),
2612 status: "ACTIVE".to_string(),
2613 number_of_ca_certificates: 1,
2614 total_revoked_entries: 0,
2615 created_time: Utc::now(),
2616 ca_certificates_bundle: Some(format!("s3://{bucket}/{key}").into_bytes()),
2617 revocations: BTreeMap::new(),
2618 next_revocation_id: 1,
2619 tags,
2620 };
2621 state.trust_stores.insert(arn.clone(), ts);
2622 Ok(ProvisionResult::new(arn.clone())
2623 .with("TrustStoreArn", arn)
2624 .with("Name", name)
2625 .with("Status", "ACTIVE".to_string()))
2626 }
2627
2628 fn delete_elbv2_trust_store(&self, physical_id: &str) -> Result<(), String> {
2629 let mut accounts = self.elbv2_state.write();
2630 let state = accounts.get_or_create(&self.account_id);
2631 state.trust_stores.remove(physical_id);
2632 Ok(())
2633 }
2634
2635 fn update_elbv2_load_balancer(
2640 &self,
2641 existing: &StackResource,
2642 resource: &ResourceDefinition,
2643 ) -> Result<ProvisionResult, String> {
2644 let props = &resource.properties;
2645 let arn = existing.physical_id.clone();
2646 let mut accounts = self.elbv2_state.write();
2647 let state = accounts.get_or_create(&self.account_id);
2648 let lb = state
2649 .load_balancers
2650 .get_mut(&arn)
2651 .ok_or_else(|| format!("LoadBalancer {arn} no longer exists"))?;
2652 if let Some(arr) = props.get("SecurityGroups").and_then(|v| v.as_array()) {
2653 lb.security_groups = arr
2654 .iter()
2655 .filter_map(|s| s.as_str().map(|s| s.to_string()))
2656 .collect();
2657 }
2658 if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
2659 lb.ip_address_type = s.to_string();
2660 }
2661 if let Some(arr) = props.get("Subnets").and_then(|v| v.as_array()) {
2662 let mut zones: Vec<fakecloud_elbv2::AvailabilityZone> = Vec::new();
2663 for s in arr {
2664 if let Some(subnet_id) = s.as_str() {
2665 zones.push(fakecloud_elbv2::AvailabilityZone {
2666 zone_name: format!("{}a", self.region),
2667 subnet_id: subnet_id.to_string(),
2668 outpost_id: None,
2669 load_balancer_addresses: Vec::new(),
2670 source_nat_ipv6_prefixes: Vec::new(),
2671 });
2672 }
2673 }
2674 lb.availability_zones = zones;
2675 }
2676 if props.get("Tags").is_some() {
2677 lb.tags = parse_elb_tags(props.get("Tags"));
2678 }
2679 let name = lb.name.clone();
2680 let dns_name = lb.dns_name.clone();
2681 let canonical = lb.canonical_hosted_zone_id.clone();
2682 let lb_full = arn.rsplit("loadbalancer/").next().unwrap_or("").to_string();
2683 Ok(ProvisionResult::new(arn.clone())
2684 .with("LoadBalancerArn", arn)
2685 .with("LoadBalancerFullName", lb_full)
2686 .with("LoadBalancerName", name)
2687 .with("DNSName", dns_name)
2688 .with("CanonicalHostedZoneID", canonical))
2689 }
2690
2691 fn update_elbv2_target_group(
2694 &self,
2695 existing: &StackResource,
2696 resource: &ResourceDefinition,
2697 ) -> Result<ProvisionResult, String> {
2698 let props = &resource.properties;
2699 let arn = existing.physical_id.clone();
2700 let mut accounts = self.elbv2_state.write();
2701 let state = accounts.get_or_create(&self.account_id);
2702 let tg = state
2703 .target_groups
2704 .get_mut(&arn)
2705 .ok_or_else(|| format!("TargetGroup {arn} no longer exists"))?;
2706 if let Some(s) = props.get("HealthCheckProtocol").and_then(|v| v.as_str()) {
2707 tg.health_check_protocol = Some(s.to_string());
2708 }
2709 if let Some(s) = props.get("HealthCheckPort").and_then(|v| v.as_str()) {
2710 tg.health_check_port = Some(s.to_string());
2711 }
2712 if let Some(b) = props.get("HealthCheckEnabled").and_then(|v| v.as_bool()) {
2713 tg.health_check_enabled = b;
2714 }
2715 if let Some(s) = props.get("HealthCheckPath").and_then(|v| v.as_str()) {
2716 tg.health_check_path = Some(s.to_string());
2717 }
2718 if let Some(n) = props.get("HealthCheckIntervalSeconds").and_then(cfn_as_i64) {
2719 tg.health_check_interval_seconds = n as i32;
2720 }
2721 if let Some(n) = props.get("HealthCheckTimeoutSeconds").and_then(cfn_as_i64) {
2722 tg.health_check_timeout_seconds = n as i32;
2723 }
2724 if let Some(n) = props.get("HealthyThresholdCount").and_then(cfn_as_i64) {
2725 tg.healthy_threshold_count = n as i32;
2726 }
2727 if let Some(n) = props.get("UnhealthyThresholdCount").and_then(cfn_as_i64) {
2728 tg.unhealthy_threshold_count = n as i32;
2729 }
2730 if let Some(matcher) = props.get("Matcher") {
2731 tg.matcher_http_code = matcher
2732 .get("HttpCode")
2733 .and_then(|v| v.as_str())
2734 .map(|s| s.to_string());
2735 tg.matcher_grpc_code = matcher
2736 .get("GrpcCode")
2737 .and_then(|v| v.as_str())
2738 .map(|s| s.to_string());
2739 }
2740 if props.get("Tags").is_some() {
2741 tg.tags = parse_elb_tags(props.get("Tags"));
2742 }
2743 let name = tg.name.clone();
2744 let tg_full = arn
2745 .rsplit("targetgroup/")
2746 .next()
2747 .map(|s| format!("targetgroup/{s}"))
2748 .unwrap_or_default();
2749 Ok(ProvisionResult::new(arn.clone())
2750 .with("TargetGroupArn", arn)
2751 .with("TargetGroupName", name)
2752 .with("TargetGroupFullName", tg_full))
2753 }
2754
2755 fn update_elbv2_listener(
2758 &self,
2759 existing: &StackResource,
2760 resource: &ResourceDefinition,
2761 ) -> Result<ProvisionResult, String> {
2762 let props = &resource.properties;
2763 let arn = existing.physical_id.clone();
2764 let new_default_actions = props
2765 .get("DefaultActions")
2766 .map(|v| parse_elb_actions(Some(v)));
2767 let mut accounts = self.elbv2_state.write();
2768 let state = accounts.get_or_create(&self.account_id);
2769 let listener = state
2770 .listeners
2771 .get_mut(&arn)
2772 .ok_or_else(|| format!("Listener {arn} no longer exists"))?;
2773 if let Some(n) = props.get("Port").and_then(cfn_as_i64) {
2774 listener.port = Some(n as i32);
2775 }
2776 if let Some(s) = props.get("Protocol").and_then(|v| v.as_str()) {
2777 listener.protocol = Some(s.to_string());
2778 }
2779 if let Some(s) = props.get("SslPolicy").and_then(|v| v.as_str()) {
2780 listener.ssl_policy = Some(s.to_string());
2781 }
2782 if let Some(actions) = new_default_actions {
2783 listener.default_actions = actions;
2784 }
2785 if props.get("Tags").is_some() {
2786 listener.tags = parse_elb_tags(props.get("Tags"));
2787 }
2788 Ok(ProvisionResult::new(arn.clone()).with("ListenerArn", arn))
2789 }
2790
2791 fn update_elbv2_listener_rule(
2794 &self,
2795 existing: &StackResource,
2796 resource: &ResourceDefinition,
2797 ) -> Result<ProvisionResult, String> {
2798 let props = &resource.properties;
2799 let arn = existing.physical_id.clone();
2800 let new_actions = props.get("Actions").map(|v| parse_elb_actions(Some(v)));
2801 let new_conditions = props
2802 .get("Conditions")
2803 .map(|v| parse_elb_rule_conditions(Some(v)));
2804 let mut accounts = self.elbv2_state.write();
2805 let state = accounts.get_or_create(&self.account_id);
2806 let rule = state
2807 .rules
2808 .get_mut(&arn)
2809 .ok_or_else(|| format!("ListenerRule {arn} no longer exists"))?;
2810 if let Some(v) = props.get("Priority") {
2811 rule.priority = if let Some(s) = v.as_str() {
2812 s.to_string()
2813 } else if let Some(n) = v.as_i64() {
2814 n.to_string()
2815 } else {
2816 rule.priority.clone()
2817 };
2818 }
2819 if let Some(actions) = new_actions {
2820 rule.actions = actions;
2821 }
2822 if let Some(conditions) = new_conditions {
2823 rule.conditions = conditions;
2824 }
2825 if props.get("Tags").is_some() {
2826 rule.tags = parse_elb_tags(props.get("Tags"));
2827 }
2828 Ok(ProvisionResult::new(arn.clone()).with("RuleArn", arn))
2829 }
2830
2831 fn update_elbv2_listener_certificate(
2836 &self,
2837 existing: &StackResource,
2838 resource: &ResourceDefinition,
2839 ) -> Result<ProvisionResult, String> {
2840 let props = &resource.properties;
2841 let physical_id = existing.physical_id.clone();
2842 let listener_arn = props
2843 .get("ListenerArn")
2844 .and_then(|v| v.as_str())
2845 .map(|s| s.to_string())
2846 .or_else(|| physical_id.split_once('#').map(|(l, _)| l.to_string()))
2847 .ok_or_else(|| "ListenerArn is required".to_string())?;
2848 let new_certs: Vec<String> = props
2849 .get("Certificates")
2850 .and_then(|v| v.as_array())
2851 .map(|arr| {
2852 arr.iter()
2853 .filter_map(|c| c.get("CertificateArn").and_then(|v| v.as_str()))
2854 .map(|s| s.to_string())
2855 .collect()
2856 })
2857 .unwrap_or_default();
2858 if new_certs.is_empty() {
2859 return Err("Certificates must contain at least one CertificateArn".to_string());
2860 }
2861
2862 let prev_certs: Vec<String> = physical_id
2864 .split_once('#')
2865 .map(|(_, list)| list.split(',').map(|s| s.to_string()).collect())
2866 .unwrap_or_default();
2867
2868 let mut accounts = self.elbv2_state.write();
2869 let state = accounts.get_or_create(&self.account_id);
2870 let listener = state
2871 .listeners
2872 .get_mut(&listener_arn)
2873 .ok_or_else(|| format!("Listener {listener_arn} does not exist"))?;
2874 listener
2875 .certificates
2876 .retain(|c| !prev_certs.iter().any(|p| p == &c.certificate_arn));
2877 for arn in &new_certs {
2878 listener.certificates.retain(|c| &c.certificate_arn != arn);
2879 listener.certificates.push(fakecloud_elbv2::Certificate {
2880 certificate_arn: arn.clone(),
2881 is_default: false,
2882 });
2883 }
2884 Ok(ProvisionResult::new(format!(
2885 "{}#{}",
2886 listener_arn,
2887 new_certs.join(",")
2888 )))
2889 }
2890
2891 fn update_elbv2_trust_store(
2894 &self,
2895 existing: &StackResource,
2896 resource: &ResourceDefinition,
2897 ) -> Result<ProvisionResult, String> {
2898 let props = &resource.properties;
2899 let arn = existing.physical_id.clone();
2900 let mut accounts = self.elbv2_state.write();
2901 let state = accounts.get_or_create(&self.account_id);
2902 let ts = state
2903 .trust_stores
2904 .get_mut(&arn)
2905 .ok_or_else(|| format!("TrustStore {arn} no longer exists"))?;
2906 let new_bucket = props
2907 .get("CaCertificatesBundleS3Bucket")
2908 .and_then(|v| v.as_str());
2909 let new_key = props
2910 .get("CaCertificatesBundleS3Key")
2911 .and_then(|v| v.as_str());
2912 if let (Some(b), Some(k)) = (new_bucket, new_key) {
2913 ts.ca_certificates_bundle = Some(format!("s3://{b}/{k}").into_bytes());
2914 }
2915 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
2916 ts.tags = arr
2917 .iter()
2918 .filter_map(|t| {
2919 let k = t.get("Key").and_then(|v| v.as_str())?;
2920 let v = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
2921 Some(fakecloud_elbv2::Tag {
2922 key: k.to_string(),
2923 value: v.to_string(),
2924 })
2925 })
2926 .collect();
2927 }
2928 let name = ts.name.clone();
2929 let status = ts.status.clone();
2930 Ok(ProvisionResult::new(arn.clone())
2931 .with("TrustStoreArn", arn)
2932 .with("Name", name)
2933 .with("Status", status))
2934 }
2935
2936 fn get_att_elbv2_load_balancer(&self, physical_id: &str, attribute: &str) -> Option<String> {
2938 let mut accounts = self.elbv2_state.write();
2939 let state = accounts.get_or_create(&self.account_id);
2940 let lb = state.load_balancers.get(physical_id)?;
2941 let lb_full = lb
2942 .arn
2943 .rsplit("loadbalancer/")
2944 .next()
2945 .unwrap_or("")
2946 .to_string();
2947 match attribute {
2948 "Arn" | "LoadBalancerArn" => Some(lb.arn.clone()),
2949 "DNSName" => Some(lb.dns_name.clone()),
2950 "CanonicalHostedZoneID" => Some(lb.canonical_hosted_zone_id.clone()),
2951 "LoadBalancerFullName" => Some(lb_full),
2952 "LoadBalancerName" => Some(lb.name.clone()),
2953 "SecurityGroups" => Some(lb.security_groups.join(",")),
2954 _ => None,
2955 }
2956 }
2957
2958 fn get_att_elbv2_target_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
2960 let mut accounts = self.elbv2_state.write();
2961 let state = accounts.get_or_create(&self.account_id);
2962 let tg = state.target_groups.get(physical_id)?;
2963 let tg_full = tg
2964 .arn
2965 .rsplit("targetgroup/")
2966 .next()
2967 .map(|s| format!("targetgroup/{s}"))
2968 .unwrap_or_default();
2969 match attribute {
2970 "TargetGroupArn" => Some(tg.arn.clone()),
2971 "TargetGroupName" => Some(tg.name.clone()),
2972 "TargetGroupFullName" => Some(tg_full),
2973 "LoadBalancerArns" => Some(tg.load_balancer_arns.join(",")),
2974 _ => None,
2975 }
2976 }
2977
2978 fn get_att_elbv2_listener(&self, physical_id: &str, attribute: &str) -> Option<String> {
2980 let mut accounts = self.elbv2_state.write();
2981 let state = accounts.get_or_create(&self.account_id);
2982 let listener = state.listeners.get(physical_id)?;
2983 match attribute {
2984 "Arn" | "ListenerArn" => Some(listener.arn.clone()),
2985 _ => None,
2986 }
2987 }
2988
2989 fn get_att_elbv2_listener_rule(&self, physical_id: &str, attribute: &str) -> Option<String> {
2991 let mut accounts = self.elbv2_state.write();
2992 let state = accounts.get_or_create(&self.account_id);
2993 let rule = state.rules.get(physical_id)?;
2994 match attribute {
2995 "RuleArn" => Some(rule.arn.clone()),
2996 "IsDefault" => Some(rule.is_default.to_string()),
2997 _ => None,
2998 }
2999 }
3000
3001 fn get_att_elbv2_trust_store(&self, physical_id: &str, attribute: &str) -> Option<String> {
3003 let mut accounts = self.elbv2_state.write();
3004 let state = accounts.get_or_create(&self.account_id);
3005 let ts = state.trust_stores.get(physical_id)?;
3006 match attribute {
3007 "TrustStoreArn" => Some(ts.arn.clone()),
3008 "Name" => Some(ts.name.clone()),
3009 "Status" => Some(ts.status.clone()),
3010 "NumberOfCaCertificates" => Some(ts.number_of_ca_certificates.to_string()),
3011 "TotalRevokedEntries" => Some(ts.total_revoked_entries.to_string()),
3012 _ => None,
3013 }
3014 }
3015
3016 fn create_organization(
3019 &self,
3020 resource: &ResourceDefinition,
3021 ) -> Result<ProvisionResult, String> {
3022 let props = &resource.properties;
3023 let feature_set = props
3024 .get("FeatureSet")
3025 .and_then(|v| v.as_str())
3026 .unwrap_or("ALL")
3027 .to_string();
3028
3029 let mut org = self.organizations_state.write();
3030 if org.is_some() {
3031 return Err("Organization already exists; only one per fakecloud process".to_string());
3032 }
3033 let mut state = OrganizationState::bootstrap(&self.account_id);
3034 state.feature_set = feature_set;
3035 let org_id = state.org_id.clone();
3036 let org_arn = state.org_arn.clone();
3037 let mgmt_arn = state.management_account_arn.clone();
3038 let root_id = state.root_id.clone();
3039 *org = Some(state);
3040
3041 Ok(ProvisionResult::new(org_id.clone())
3042 .with("Id", org_id)
3043 .with("Arn", org_arn)
3044 .with("ManagementAccountArn", mgmt_arn)
3045 .with("RootId", root_id))
3046 }
3047
3048 fn delete_organization(&self, _physical_id: &str) -> Result<(), String> {
3049 let mut org = self.organizations_state.write();
3050 *org = None;
3051 Ok(())
3052 }
3053
3054 fn create_organization_unit(
3055 &self,
3056 resource: &ResourceDefinition,
3057 ) -> Result<ProvisionResult, String> {
3058 let props = &resource.properties;
3059 let name = props
3060 .get("Name")
3061 .and_then(|v| v.as_str())
3062 .unwrap_or(&resource.logical_id)
3063 .to_string();
3064 let parent_id = props
3065 .get("ParentId")
3066 .and_then(|v| v.as_str())
3067 .ok_or_else(|| "ParentId is required".to_string())?
3068 .to_string();
3069
3070 let mut org_lock = self.organizations_state.write();
3071 let org = org_lock
3072 .as_mut()
3073 .ok_or_else(|| "Organization not yet created".to_string())?;
3074 let resolved_parent_id = if parent_id == org.root_id || org.ous.contains_key(&parent_id) {
3076 parent_id
3077 } else {
3078 return Err(format!("Parent {parent_id} does not exist"));
3079 };
3080 let id_suffix: String = Uuid::new_v4()
3081 .simple()
3082 .to_string()
3083 .chars()
3084 .take(8)
3085 .collect();
3086 let id = format!("ou-{}-{}", &org.root_id[2..], id_suffix);
3087 let arn = format!(
3088 "arn:aws:organizations::{}:ou/{}/{}",
3089 org.management_account_id, org.org_id, id
3090 );
3091 org.ous.insert(
3092 id.clone(),
3093 OrganizationalUnit {
3094 id: id.clone(),
3095 arn: arn.clone(),
3096 name: name.clone(),
3097 parent_id: resolved_parent_id,
3098 },
3099 );
3100 Ok(ProvisionResult::new(id.clone())
3101 .with("Id", id)
3102 .with("Arn", arn)
3103 .with("Name", name))
3104 }
3105
3106 fn delete_organization_unit(&self, physical_id: &str) -> Result<(), String> {
3107 let mut org_lock = self.organizations_state.write();
3108 if let Some(org) = org_lock.as_mut() {
3109 org.ous.remove(physical_id);
3110 org.attachments.remove(physical_id);
3111 }
3112 Ok(())
3113 }
3114
3115 fn create_organization_account(
3119 &self,
3120 resource: &ResourceDefinition,
3121 ) -> Result<ProvisionResult, String> {
3122 let props = &resource.properties;
3123 let email = props
3124 .get("Email")
3125 .and_then(|v| v.as_str())
3126 .ok_or_else(|| "Email is required".to_string())?
3127 .to_string();
3128 let name = props
3129 .get("AccountName")
3130 .and_then(|v| v.as_str())
3131 .ok_or_else(|| "AccountName is required".to_string())?
3132 .to_string();
3133 let parent_ids: Vec<String> = props
3134 .get("ParentIds")
3135 .and_then(|v| v.as_array())
3136 .map(|arr| {
3137 arr.iter()
3138 .filter_map(|v| v.as_str().map(|s| s.to_string()))
3139 .collect()
3140 })
3141 .unwrap_or_default();
3142 let tags: Vec<(String, String)> = props
3143 .get("Tags")
3144 .and_then(|v| v.as_array())
3145 .map(|arr| {
3146 arr.iter()
3147 .filter_map(|t| {
3148 let k = t.get("Key").and_then(|v| v.as_str())?;
3149 let val = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
3150 Some((k.to_string(), val.to_string()))
3151 })
3152 .collect()
3153 })
3154 .unwrap_or_default();
3155
3156 let mut org_lock = self.organizations_state.write();
3157 let org = org_lock
3158 .as_mut()
3159 .ok_or_else(|| "Organization not yet created".to_string())?;
3160 let pending = org.begin_create_account(&email, &name, None);
3165 let status = org.complete_create_account(&pending.id).unwrap_or(pending);
3166 let account_id = status
3167 .account_id
3168 .clone()
3169 .ok_or_else(|| "create_account did not return an account id".to_string())?;
3170 let account_arn = org
3171 .accounts
3172 .get(&account_id)
3173 .map(|a| a.arn.clone())
3174 .unwrap_or_default();
3175 let joined_method = org
3176 .accounts
3177 .get(&account_id)
3178 .map(|a| a.joined_method.clone())
3179 .unwrap_or_else(|| "CREATED".to_string());
3180 let joined_timestamp = org
3181 .accounts
3182 .get(&account_id)
3183 .map(|a| a.joined_timestamp.to_rfc3339())
3184 .unwrap_or_default();
3185 let acct_status = org
3186 .accounts
3187 .get(&account_id)
3188 .map(|a| a.status.clone())
3189 .unwrap_or_else(|| "ACTIVE".to_string());
3190
3191 if let Some(parent) = parent_ids.first() {
3192 let source = org
3193 .accounts
3194 .get(&account_id)
3195 .map(|a| a.parent_id.clone())
3196 .unwrap_or_else(|| org.root_id.clone());
3197 if parent != &source {
3198 org.move_account(&account_id, &source, parent)
3199 .map_err(|e| format!("Failed to move account to parent {parent}: {e:?}"))?;
3200 }
3201 }
3202
3203 if !tags.is_empty() {
3204 org.set_resource_tags(&account_id, &tags);
3205 }
3206
3207 Ok(ProvisionResult::new(account_id.clone())
3208 .with("AccountId", account_id)
3209 .with("AccountName", name)
3210 .with("Email", email)
3211 .with("Arn", account_arn)
3212 .with("JoinedMethod", joined_method)
3213 .with("JoinedTimestamp", joined_timestamp)
3214 .with("Status", acct_status))
3215 }
3216
3217 fn delete_organization_account(&self, physical_id: &str) -> Result<(), String> {
3221 let mut org_lock = self.organizations_state.write();
3222 if let Some(org) = org_lock.as_mut() {
3223 let _ = org.close_account(physical_id);
3224 }
3225 Ok(())
3226 }
3227
3228 fn create_organization_policy(
3229 &self,
3230 resource: &ResourceDefinition,
3231 ) -> Result<ProvisionResult, String> {
3232 let props = &resource.properties;
3233 let name = props
3234 .get("Name")
3235 .and_then(|v| v.as_str())
3236 .unwrap_or(&resource.logical_id)
3237 .to_string();
3238 let description = props
3239 .get("Description")
3240 .and_then(|v| v.as_str())
3241 .unwrap_or("")
3242 .to_string();
3243 let policy_type = props
3244 .get("Type")
3245 .and_then(|v| v.as_str())
3246 .unwrap_or(POLICY_TYPE_SCP)
3247 .to_string();
3248 let content = props
3249 .get("Content")
3250 .map(|v| {
3251 if v.is_string() {
3252 v.as_str().unwrap_or("").to_string()
3253 } else {
3254 serde_json::to_string(v).unwrap_or_default()
3255 }
3256 })
3257 .unwrap_or_default();
3258 let target_ids: Vec<String> = props
3259 .get("TargetIds")
3260 .and_then(|v| v.as_array())
3261 .map(|arr| {
3262 arr.iter()
3263 .filter_map(|t| t.as_str().map(|s| s.to_string()))
3264 .collect()
3265 })
3266 .unwrap_or_default();
3267
3268 let mut org_lock = self.organizations_state.write();
3269 let org = org_lock
3270 .as_mut()
3271 .ok_or_else(|| "Organization not yet created".to_string())?;
3272 let id_suffix: String = Uuid::new_v4()
3273 .simple()
3274 .to_string()
3275 .chars()
3276 .take(8)
3277 .collect();
3278 let id = format!("p-{}", id_suffix);
3279 let arn = format!(
3280 "arn:aws:organizations::{}:policy/{}/{}/{}",
3281 org.management_account_id,
3282 org.org_id,
3283 policy_type.to_lowercase(),
3284 id
3285 );
3286 org.policies.insert(
3287 id.clone(),
3288 OrgPolicy {
3289 id: id.clone(),
3290 arn: arn.clone(),
3291 name: name.clone(),
3292 description,
3293 policy_type,
3294 aws_managed: false,
3295 content,
3296 },
3297 );
3298 for target in target_ids {
3299 org.attachments
3300 .entry(target)
3301 .or_default()
3302 .insert(id.clone());
3303 }
3304 Ok(ProvisionResult::new(id.clone())
3305 .with("Id", id)
3306 .with("Arn", arn)
3307 .with("Name", name))
3308 }
3309
3310 fn delete_organization_policy(&self, physical_id: &str) -> Result<(), String> {
3311 let mut org_lock = self.organizations_state.write();
3312 if let Some(org) = org_lock.as_mut() {
3313 org.policies.remove(physical_id);
3314 for attachments in org.attachments.values_mut() {
3315 attachments.remove(physical_id);
3316 }
3317 }
3318 Ok(())
3319 }
3320
3321 fn create_organization_resource_policy(
3322 &self,
3323 resource: &ResourceDefinition,
3324 ) -> Result<ProvisionResult, String> {
3325 let props = &resource.properties;
3326 let content = props
3327 .get("Content")
3328 .map(|v| {
3329 if v.is_string() {
3330 v.as_str().unwrap_or("").to_string()
3331 } else {
3332 serde_json::to_string(v).unwrap_or_default()
3333 }
3334 })
3335 .ok_or_else(|| "Content is required".to_string())?;
3336
3337 let mut org_lock = self.organizations_state.write();
3338 let org = org_lock
3339 .as_mut()
3340 .ok_or_else(|| "Organization not yet created".to_string())?;
3341 org.resource_policy = Some(content);
3342 let arn = format!(
3343 "arn:aws:organizations::{}:resourcepolicy/{}/rp",
3344 org.management_account_id, org.org_id
3345 );
3346 Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
3347 }
3348
3349 fn delete_organization_resource_policy(&self, _physical_id: &str) -> Result<(), String> {
3350 let mut org_lock = self.organizations_state.write();
3351 if let Some(org) = org_lock.as_mut() {
3352 org.resource_policy = None;
3353 }
3354 Ok(())
3355 }
3356
3357 fn delete_log_group(&self, physical_id: &str) -> Result<(), String> {
3358 let mut logs_accounts = self.logs_state.write();
3359 let state = logs_accounts.default_mut();
3360 let name = state
3362 .log_groups
3363 .iter()
3364 .find(|(_, g)| g.arn == physical_id)
3365 .map(|(name, _)| name.clone());
3366 if let Some(name) = name {
3367 state.log_groups.remove(&name);
3368 }
3369 Ok(())
3370 }
3371
3372 fn create_log_stream(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
3373 let props = &resource.properties;
3374 let log_group_name = props
3375 .get("LogGroupName")
3376 .and_then(|v| v.as_str())
3377 .map(parse_log_group_name)
3378 .ok_or_else(|| "LogGroupName is required".to_string())?;
3379 let log_stream_name = props
3380 .get("LogStreamName")
3381 .and_then(|v| v.as_str())
3382 .unwrap_or(&resource.logical_id)
3383 .to_string();
3384
3385 let mut logs_accounts = self.logs_state.write();
3386 let state = logs_accounts.get_or_create(&self.account_id);
3387 let group = state
3388 .log_groups
3389 .get_mut(&log_group_name)
3390 .ok_or_else(|| format!("Log group {log_group_name} does not exist"))?;
3391 let arn = format!(
3392 "arn:aws:logs:{}:{}:log-group:{}:log-stream:{}",
3393 self.region, self.account_id, log_group_name, log_stream_name
3394 );
3395 if group.log_streams.contains_key(&log_stream_name) {
3396 return Err(format!(
3397 "Log stream {log_stream_name} already exists in {log_group_name}"
3398 ));
3399 }
3400 group.log_streams.insert(
3401 log_stream_name.clone(),
3402 LogStream {
3403 name: log_stream_name.clone(),
3404 arn,
3405 creation_time: Utc::now().timestamp_millis(),
3406 first_event_timestamp: None,
3407 last_event_timestamp: None,
3408 last_ingestion_time: None,
3409 upload_sequence_token: String::new(),
3410 events: Vec::new(),
3411 },
3412 );
3413
3414 let physical_id = format!("{log_group_name}|{log_stream_name}");
3416 Ok(ProvisionResult::new(physical_id))
3417 }
3418
3419 fn delete_log_stream(&self, physical_id: &str) -> Result<(), String> {
3420 let mut logs_accounts = self.logs_state.write();
3421 let state = logs_accounts.get_or_create(&self.account_id);
3422 if let Some((group_name, stream_name)) = physical_id.split_once('|') {
3423 if let Some(group) = state.log_groups.get_mut(group_name) {
3424 group.log_streams.remove(stream_name);
3425 }
3426 }
3427 Ok(())
3428 }
3429
3430 fn create_metric_filter(
3431 &self,
3432 resource: &ResourceDefinition,
3433 ) -> Result<ProvisionResult, String> {
3434 let props = &resource.properties;
3435 let log_group_name = props
3436 .get("LogGroupName")
3437 .and_then(|v| v.as_str())
3438 .map(parse_log_group_name)
3439 .ok_or_else(|| "LogGroupName is required".to_string())?;
3440 let filter_name = props
3441 .get("FilterName")
3442 .and_then(|v| v.as_str())
3443 .unwrap_or(&resource.logical_id)
3444 .to_string();
3445 let filter_pattern = props
3446 .get("FilterPattern")
3447 .and_then(|v| v.as_str())
3448 .unwrap_or("")
3449 .to_string();
3450
3451 let mut transformations: Vec<MetricTransformation> = Vec::new();
3452 if let Some(arr) = props
3453 .get("MetricTransformations")
3454 .and_then(|v| v.as_array())
3455 {
3456 for t in arr {
3457 let metric_name = t
3458 .get("MetricName")
3459 .and_then(|v| v.as_str())
3460 .unwrap_or("")
3461 .to_string();
3462 let metric_namespace = t
3463 .get("MetricNamespace")
3464 .and_then(|v| v.as_str())
3465 .unwrap_or("")
3466 .to_string();
3467 let metric_value = t
3468 .get("MetricValue")
3469 .and_then(|v| v.as_str())
3470 .unwrap_or("1")
3471 .to_string();
3472 let default_value = t.get("DefaultValue").and_then(|v| v.as_f64());
3473 let unit = t
3474 .get("Unit")
3475 .and_then(|v| v.as_str())
3476 .map(|s| s.to_string());
3477 transformations.push(MetricTransformation {
3478 metric_name,
3479 metric_namespace,
3480 metric_value,
3481 default_value,
3482 unit,
3483 });
3484 }
3485 }
3486
3487 let mut logs_accounts = self.logs_state.write();
3488 let state = logs_accounts.get_or_create(&self.account_id);
3489 if !state.log_groups.contains_key(&log_group_name) {
3490 return Err(format!("Log group {log_group_name} does not exist"));
3491 }
3492 state
3493 .metric_filters
3494 .retain(|f| !(f.log_group_name == log_group_name && f.filter_name == filter_name));
3495 state.metric_filters.push(MetricFilter {
3496 filter_name: filter_name.clone(),
3497 filter_pattern,
3498 log_group_name: log_group_name.clone(),
3499 metric_transformations: transformations,
3500 creation_time: Utc::now().timestamp_millis(),
3501 });
3502
3503 Ok(ProvisionResult::new(format!(
3504 "{log_group_name}|{filter_name}"
3505 )))
3506 }
3507
3508 fn delete_metric_filter(&self, physical_id: &str) -> Result<(), String> {
3509 let mut logs_accounts = self.logs_state.write();
3510 let state = logs_accounts.get_or_create(&self.account_id);
3511 if let Some((group_name, filter_name)) = physical_id.split_once('|') {
3512 state
3513 .metric_filters
3514 .retain(|f| !(f.log_group_name == group_name && f.filter_name == filter_name));
3515 }
3516 Ok(())
3517 }
3518
3519 fn create_subscription_filter(
3520 &self,
3521 resource: &ResourceDefinition,
3522 ) -> Result<ProvisionResult, String> {
3523 let props = &resource.properties;
3524 let log_group_name = props
3525 .get("LogGroupName")
3526 .and_then(|v| v.as_str())
3527 .map(parse_log_group_name)
3528 .ok_or_else(|| "LogGroupName is required".to_string())?;
3529 let filter_name = props
3530 .get("FilterName")
3531 .and_then(|v| v.as_str())
3532 .unwrap_or(&resource.logical_id)
3533 .to_string();
3534 let filter_pattern = props
3535 .get("FilterPattern")
3536 .and_then(|v| v.as_str())
3537 .unwrap_or("")
3538 .to_string();
3539 let destination_arn = props
3540 .get("DestinationArn")
3541 .and_then(|v| v.as_str())
3542 .ok_or_else(|| "DestinationArn is required".to_string())?
3543 .to_string();
3544 let role_arn = props
3545 .get("RoleArn")
3546 .and_then(|v| v.as_str())
3547 .map(|s| s.to_string());
3548 let distribution = props
3549 .get("Distribution")
3550 .and_then(|v| v.as_str())
3551 .unwrap_or("ByLogStream")
3552 .to_string();
3553
3554 let mut logs_accounts = self.logs_state.write();
3555 let state = logs_accounts.get_or_create(&self.account_id);
3556 let group = state
3557 .log_groups
3558 .get_mut(&log_group_name)
3559 .ok_or_else(|| format!("Log group {log_group_name} does not exist"))?;
3560 group
3561 .subscription_filters
3562 .retain(|f| f.filter_name != filter_name);
3563 group.subscription_filters.push(SubscriptionFilter {
3564 filter_name: filter_name.clone(),
3565 log_group_name: log_group_name.clone(),
3566 filter_pattern,
3567 destination_arn,
3568 role_arn,
3569 distribution,
3570 creation_time: Utc::now().timestamp_millis(),
3571 });
3572
3573 Ok(ProvisionResult::new(format!(
3574 "{log_group_name}|{filter_name}"
3575 )))
3576 }
3577
3578 fn delete_subscription_filter(&self, physical_id: &str) -> Result<(), String> {
3579 let mut logs_accounts = self.logs_state.write();
3580 let state = logs_accounts.get_or_create(&self.account_id);
3581 if let Some((group_name, filter_name)) = physical_id.split_once('|') {
3582 if let Some(group) = state.log_groups.get_mut(group_name) {
3583 group
3584 .subscription_filters
3585 .retain(|f| f.filter_name != filter_name);
3586 }
3587 }
3588 Ok(())
3589 }
3590
3591 fn invoke_lambda_sync(&self, function_arn: &str, payload: &str) -> Result<(), String> {
3595 let delivery = self.delivery.clone();
3596 let function_arn = function_arn.to_string();
3597 let payload = payload.to_string();
3598 std::thread::scope(|s| {
3599 s.spawn(|| {
3600 let rt = tokio::runtime::Builder::new_current_thread()
3601 .enable_all()
3602 .build()
3603 .map_err(|e| format!("Failed to create runtime: {e}"))?;
3604 rt.block_on(async {
3605 match delivery.invoke_lambda(&function_arn, &payload).await {
3606 Some(Ok(_)) => {
3607 tracing::info!(
3608 "Custom resource Lambda {} invoked successfully",
3609 function_arn
3610 );
3611 Ok(())
3612 }
3613 Some(Err(e)) => {
3614 tracing::warn!(
3615 "Custom resource Lambda {} invocation failed: {e}",
3616 function_arn
3617 );
3618 Err(format!("Lambda invocation failed: {e}"))
3619 }
3620 None => {
3621 tracing::warn!(
3622 "No Lambda delivery configured; skipping custom resource invocation for {}",
3623 function_arn
3624 );
3625 Ok(())
3626 }
3627 }
3628 })
3629 })
3630 .join()
3631 .map_err(|_| "Lambda invocation thread panicked".to_string())?
3632 })
3633 }
3634
3635 fn create_custom_resource(&self, resource: &ResourceDefinition) -> Result<String, String> {
3636 let props = &resource.properties;
3637 let service_token = props
3638 .get("ServiceToken")
3639 .and_then(|v| v.as_str())
3640 .ok_or("Custom resource requires ServiceToken property")?;
3641
3642 let request_id = Uuid::new_v4().to_string();
3643
3644 let event = serde_json::json!({
3646 "RequestType": "Create",
3647 "ServiceToken": service_token,
3648 "StackId": self.stack_id,
3649 "RequestId": request_id,
3650 "ResourceType": resource.resource_type,
3651 "LogicalResourceId": resource.logical_id,
3652 "ResourceProperties": props,
3653 });
3654
3655 let payload = serde_json::to_string(&event).map_err(|e| e.to_string())?;
3656 self.invoke_lambda_sync(service_token, &payload)?;
3657
3658 let physical_id = format!("{}-{}", resource.logical_id, &request_id[..8]);
3661 Ok(physical_id)
3662 }
3663
3664 fn delete_custom_resource(&self, resource: &StackResource) -> Result<(), String> {
3665 let service_token = match &resource.service_token {
3666 Some(token) => token.clone(),
3667 None => {
3668 return Ok(());
3670 }
3671 };
3672
3673 let request_id = Uuid::new_v4().to_string();
3674
3675 let event = serde_json::json!({
3676 "RequestType": "Delete",
3677 "ServiceToken": service_token,
3678 "StackId": self.stack_id,
3679 "RequestId": request_id,
3680 "ResourceType": resource.resource_type,
3681 "LogicalResourceId": resource.logical_id,
3682 "PhysicalResourceId": resource.physical_id,
3683 });
3684
3685 let payload = serde_json::to_string(&event).map_err(|e| e.to_string())?;
3686
3687 if let Err(e) = self.invoke_lambda_sync(&service_token, &payload) {
3689 tracing::warn!(
3690 "Custom resource delete Lambda invocation failed for {}: {e}",
3691 resource.logical_id
3692 );
3693 }
3694 Ok(())
3695 }
3696
3697 fn create_application_autoscaling_scalable_target(
3700 &self,
3701 resource: &ResourceDefinition,
3702 ) -> Result<ProvisionResult, String> {
3703 let props = &resource.properties;
3704 let service_namespace = props
3705 .get("ServiceNamespace")
3706 .and_then(|v| v.as_str())
3707 .ok_or_else(|| "ServiceNamespace is required".to_string())?
3708 .to_string();
3709 let resource_id = props
3710 .get("ResourceId")
3711 .and_then(|v| v.as_str())
3712 .ok_or_else(|| "ResourceId is required".to_string())?
3713 .to_string();
3714 let scalable_dimension = props
3715 .get("ScalableDimension")
3716 .and_then(|v| v.as_str())
3717 .ok_or_else(|| "ScalableDimension is required".to_string())?
3718 .to_string();
3719 let min_capacity = props
3720 .get("MinCapacity")
3721 .and_then(|v| v.as_i64())
3722 .map(|n| n as i32)
3723 .ok_or_else(|| "MinCapacity is required".to_string())?;
3724 let max_capacity = props
3725 .get("MaxCapacity")
3726 .and_then(|v| v.as_i64())
3727 .map(|n| n as i32)
3728 .ok_or_else(|| "MaxCapacity is required".to_string())?;
3729 if min_capacity > max_capacity {
3730 return Err("MinCapacity must be <= MaxCapacity".to_string());
3731 }
3732 let role_arn = props
3733 .get("RoleARN")
3734 .and_then(|v| v.as_str())
3735 .map(|s| s.to_string());
3736 let suspended_state = props.get("SuspendedState").map(|v| AppasSuspendedState {
3737 dynamic_scaling_in_suspended: v
3738 .get("DynamicScalingInSuspended")
3739 .and_then(|x| x.as_bool()),
3740 dynamic_scaling_out_suspended: v
3741 .get("DynamicScalingOutSuspended")
3742 .and_then(|x| x.as_bool()),
3743 scheduled_scaling_suspended: v
3744 .get("ScheduledScalingSuspended")
3745 .and_then(|x| x.as_bool()),
3746 });
3747
3748 let arn = format!(
3749 "arn:aws:application-autoscaling:{}:{}:scalable-target/{}",
3750 self.region,
3751 self.account_id,
3752 &Uuid::new_v4().simple().to_string()[..10]
3753 );
3754 let role = role_arn.unwrap_or_else(|| {
3755 let suffix = match service_namespace.as_str() {
3756 "ecs" => "ECSService",
3757 "elasticmapreduce" => "EMRContainerService",
3758 "ec2" => "EC2SpotFleetRequest",
3759 "appstream" => "ApplicationAutoScaling_AppStreamFleet",
3760 "dynamodb" => "DynamoDBTable",
3761 "rds" => "RDSCluster",
3762 "sagemaker" => "SageMakerEndpoint",
3763 "lambda" => "LambdaConcurrency",
3764 "elasticache" => "ElastiCacheRG",
3765 "cassandra" => "CassandraTable",
3766 "kafka" => "KafkaCluster",
3767 _ => "ApplicationAutoScaling_Default",
3768 };
3769 format!(
3770 "arn:aws:iam::{}:role/aws-service-role/applicationautoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_{}",
3771 self.account_id, suffix
3772 )
3773 });
3774
3775 let mut state = self.app_autoscaling_state.write();
3776 let account = state.accounts.entry(self.account_id.clone()).or_default();
3777 let key = (
3778 service_namespace.clone(),
3779 resource_id.clone(),
3780 scalable_dimension.clone(),
3781 );
3782 let target = AppasScalableTarget {
3783 arn: arn.clone(),
3784 service_namespace: service_namespace.clone(),
3785 resource_id: resource_id.clone(),
3786 scalable_dimension: scalable_dimension.clone(),
3787 min_capacity,
3788 max_capacity,
3789 role_arn: role,
3790 creation_time: Utc::now(),
3791 suspended_state,
3792 predicted_capacity: None,
3793 };
3794 account.scalable_targets.insert(key, target);
3795
3796 Ok(ProvisionResult::new(resource_id.clone())
3797 .with("ScalableTargetARN", arn)
3798 .with("ServiceNamespace", service_namespace)
3799 .with("ScalableDimension", scalable_dimension))
3800 }
3801
3802 fn create_application_autoscaling_scaling_policy(
3803 &self,
3804 resource: &ResourceDefinition,
3805 ) -> Result<ProvisionResult, String> {
3806 let props = &resource.properties;
3807 let policy_name = props
3808 .get("PolicyName")
3809 .and_then(|v| v.as_str())
3810 .ok_or_else(|| "PolicyName is required".to_string())?
3811 .to_string();
3812 let service_namespace = props
3813 .get("ServiceNamespace")
3814 .and_then(|v| v.as_str())
3815 .ok_or_else(|| "ServiceNamespace is required".to_string())?
3816 .to_string();
3817 let resource_id = props
3818 .get("ResourceId")
3819 .and_then(|v| v.as_str())
3820 .ok_or_else(|| "ResourceId is required".to_string())?
3821 .to_string();
3822 let scalable_dimension = props
3823 .get("ScalableDimension")
3824 .and_then(|v| v.as_str())
3825 .ok_or_else(|| "ScalableDimension is required".to_string())?
3826 .to_string();
3827 let policy_type = props
3828 .get("PolicyType")
3829 .and_then(|v| v.as_str())
3830 .unwrap_or("StepScaling")
3831 .to_string();
3832 let step_cfg = props.get("StepScalingPolicyConfiguration").cloned();
3833 let tt_cfg = props
3834 .get("TargetTrackingScalingPolicyConfiguration")
3835 .cloned();
3836 let pred_cfg = props.get("PredictiveScalingPolicyConfiguration").cloned();
3837
3838 let target_key = (
3839 service_namespace.clone(),
3840 resource_id.clone(),
3841 scalable_dimension.clone(),
3842 );
3843 let policy_key = (
3844 service_namespace.clone(),
3845 resource_id.clone(),
3846 scalable_dimension.clone(),
3847 policy_name.clone(),
3848 );
3849
3850 let mut state = self.app_autoscaling_state.write();
3851 let account = state.accounts.entry(self.account_id.clone()).or_default();
3852 if !account.scalable_targets.contains_key(&target_key) {
3853 return Err(format!(
3854 "No scalable target registered for ServiceNamespace={} ResourceId={} ScalableDimension={}",
3855 service_namespace, resource_id, scalable_dimension
3856 ));
3857 }
3858 let arn = format!(
3859 "arn:aws:autoscaling:{}:{}:scalingPolicy:{}:resource/{}/{}:policyName/{}",
3860 self.region,
3861 self.account_id,
3862 Uuid::new_v4(),
3863 service_namespace,
3864 resource_id,
3865 policy_name
3866 );
3867 let policy = AppasScalingPolicy {
3868 arn: arn.clone(),
3869 policy_name: policy_name.clone(),
3870 service_namespace: service_namespace.clone(),
3871 resource_id: resource_id.clone(),
3872 scalable_dimension: scalable_dimension.clone(),
3873 policy_type: policy_type.clone(),
3874 creation_time: Utc::now(),
3875 step_scaling_policy_configuration: step_cfg,
3876 target_tracking_scaling_policy_configuration: tt_cfg,
3877 predictive_scaling_policy_configuration: pred_cfg,
3878 alarms: Vec::new(),
3879 last_applied_at: None,
3880 };
3881 account.scaling_policies.insert(policy_key, policy);
3882
3883 Ok(ProvisionResult::new(arn.clone())
3884 .with("PolicyName", policy_name)
3885 .with("ServiceNamespace", service_namespace)
3886 .with("ResourceId", resource_id)
3887 .with("ScalableDimension", scalable_dimension))
3888 }
3889
3890 fn delete_application_autoscaling_scalable_target(
3891 &self,
3892 physical_id: &str,
3893 attributes: &BTreeMap<String, String>,
3894 ) -> Result<(), String> {
3895 let namespace = attributes
3896 .get("ServiceNamespace")
3897 .cloned()
3898 .ok_or_else(|| "ServiceNamespace missing in attributes".to_string())?;
3899 let resource_id = physical_id.to_string();
3900 let dimension = attributes
3901 .get("ScalableDimension")
3902 .cloned()
3903 .ok_or_else(|| "ScalableDimension missing in attributes".to_string())?;
3904 let key = (namespace, resource_id.clone(), dimension);
3905
3906 let mut state = self.app_autoscaling_state.write();
3907 let account = state.accounts.entry(self.account_id.clone()).or_default();
3908 account.scalable_targets.remove(&key);
3909 account
3910 .scaling_policies
3911 .retain(|k, _| !(k.0 == key.0 && k.1 == key.1 && k.2 == key.2));
3912 account
3913 .scheduled_actions
3914 .retain(|k, _| !(k.0 == key.0 && k.1 == key.1 && k.2 == key.2));
3915 Ok(())
3916 }
3917
3918 fn delete_application_autoscaling_scaling_policy(
3919 &self,
3920 _physical_id: &str,
3921 attributes: &BTreeMap<String, String>,
3922 ) -> Result<(), String> {
3923 let policy_name = attributes
3924 .get("PolicyName")
3925 .cloned()
3926 .ok_or_else(|| "PolicyName missing in attributes".to_string())?;
3927 let namespace = attributes
3928 .get("ServiceNamespace")
3929 .cloned()
3930 .ok_or_else(|| "ServiceNamespace missing in attributes".to_string())?;
3931 let resource_id = attributes
3932 .get("ResourceId")
3933 .cloned()
3934 .ok_or_else(|| "ResourceId missing in attributes".to_string())?;
3935 let dimension = attributes
3936 .get("ScalableDimension")
3937 .cloned()
3938 .ok_or_else(|| "ScalableDimension missing in attributes".to_string())?;
3939 let key = (namespace, resource_id, dimension, policy_name);
3940
3941 let mut state = self.app_autoscaling_state.write();
3942 let account = state.accounts.entry(self.account_id.clone()).or_default();
3943 account.scaling_policies.remove(&key);
3944 Ok(())
3945 }
3946
3947 fn create_ec_parameter_group(
3950 &self,
3951 resource: &ResourceDefinition,
3952 ) -> Result<ProvisionResult, String> {
3953 let props = &resource.properties;
3954 let name = props
3955 .get("CacheParameterGroupName")
3956 .and_then(|v| v.as_str())
3957 .unwrap_or(&resource.logical_id)
3958 .to_string();
3959 let family = props
3960 .get("CacheParameterGroupFamily")
3961 .and_then(|v| v.as_str())
3962 .unwrap_or("redis7")
3963 .to_string();
3964 let description = props
3965 .get("Description")
3966 .and_then(|v| v.as_str())
3967 .unwrap_or("")
3968 .to_string();
3969 let arn = format!(
3970 "arn:aws:elasticache:{}:{}:parametergroup:{}",
3971 self.region, self.account_id, name
3972 );
3973 let group = CacheParameterGroup {
3974 cache_parameter_group_name: name.clone(),
3975 cache_parameter_group_family: family,
3976 description,
3977 is_global: false,
3978 arn: arn.clone(),
3979 };
3980 let mut accounts = self.elasticache_state.write();
3981 let state = accounts.get_or_create(&self.account_id);
3982 state
3984 .parameter_groups
3985 .retain(|p| p.cache_parameter_group_name != name);
3986 state.parameter_groups.push(group);
3987 Ok(ProvisionResult::new(name).with("Arn", arn))
3988 }
3989
3990 fn delete_ec_parameter_group(&self, physical_id: &str) -> Result<(), String> {
3991 let mut accounts = self.elasticache_state.write();
3992 let state = accounts.get_or_create(&self.account_id);
3993 state
3994 .parameter_groups
3995 .retain(|p| p.cache_parameter_group_name != physical_id);
3996 Ok(())
3997 }
3998
3999 fn create_ec_subnet_group(
4000 &self,
4001 resource: &ResourceDefinition,
4002 ) -> Result<ProvisionResult, String> {
4003 let props = &resource.properties;
4004 let name = props
4005 .get("CacheSubnetGroupName")
4006 .and_then(|v| v.as_str())
4007 .unwrap_or(&resource.logical_id)
4008 .to_string();
4009 let description = props
4010 .get("Description")
4011 .and_then(|v| v.as_str())
4012 .unwrap_or("")
4013 .to_string();
4014 let subnet_ids: Vec<String> = props
4015 .get("SubnetIds")
4016 .and_then(|v| v.as_array())
4017 .map(|arr| {
4018 arr.iter()
4019 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4020 .collect()
4021 })
4022 .unwrap_or_default();
4023 let arn = format!(
4024 "arn:aws:elasticache:{}:{}:subnetgroup:{}",
4025 self.region, self.account_id, name
4026 );
4027 let group = CacheSubnetGroup {
4028 cache_subnet_group_name: name.clone(),
4029 cache_subnet_group_description: description,
4030 vpc_id: String::new(),
4031 subnet_ids,
4032 arn: arn.clone(),
4033 };
4034 let mut accounts = self.elasticache_state.write();
4035 let state = accounts.get_or_create(&self.account_id);
4036 state.subnet_groups.insert(name.clone(), group);
4037 Ok(ProvisionResult::new(name).with("Arn", arn))
4038 }
4039
4040 fn delete_ec_subnet_group(&self, physical_id: &str) -> Result<(), String> {
4041 let mut accounts = self.elasticache_state.write();
4042 let state = accounts.get_or_create(&self.account_id);
4043 state.subnet_groups.remove(physical_id);
4044 Ok(())
4045 }
4046
4047 fn create_ec_security_group(
4048 &self,
4049 resource: &ResourceDefinition,
4050 ) -> Result<ProvisionResult, String> {
4051 let props = &resource.properties;
4052 let name = props
4053 .get("CacheSecurityGroupName")
4054 .and_then(|v| v.as_str())
4055 .unwrap_or(&resource.logical_id)
4056 .to_string();
4057 let description = props
4058 .get("Description")
4059 .and_then(|v| v.as_str())
4060 .unwrap_or("")
4061 .to_string();
4062 let arn = format!(
4063 "arn:aws:elasticache:{}:{}:securitygroup:{}",
4064 self.region, self.account_id, name
4065 );
4066 let group = CacheSecurityGroup {
4067 cache_security_group_name: name.clone(),
4068 description,
4069 owner_id: self.account_id.clone(),
4070 arn: arn.clone(),
4071 ec2_security_groups: Vec::new(),
4072 };
4073 let mut accounts = self.elasticache_state.write();
4074 let state = accounts.get_or_create(&self.account_id);
4075 state.security_groups.insert(name.clone(), group);
4076 Ok(ProvisionResult::new(name).with("Arn", arn))
4077 }
4078
4079 fn delete_ec_security_group(&self, physical_id: &str) -> Result<(), String> {
4080 let mut accounts = self.elasticache_state.write();
4081 let state = accounts.get_or_create(&self.account_id);
4082 state.security_groups.remove(physical_id);
4083 Ok(())
4084 }
4085
4086 fn create_ec_user(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
4087 let props = &resource.properties;
4088 let user_id = props
4089 .get("UserId")
4090 .and_then(|v| v.as_str())
4091 .unwrap_or(&resource.logical_id)
4092 .to_string();
4093 let user_name = props
4094 .get("UserName")
4095 .and_then(|v| v.as_str())
4096 .unwrap_or(&user_id)
4097 .to_string();
4098 let engine = props
4099 .get("Engine")
4100 .and_then(|v| v.as_str())
4101 .unwrap_or("redis")
4102 .to_string();
4103 let access_string = props
4104 .get("AccessString")
4105 .and_then(|v| v.as_str())
4106 .unwrap_or("on ~* +@all")
4107 .to_string();
4108 let authentication_type = props
4109 .get("AuthenticationMode")
4110 .and_then(|v| v.get("Type"))
4111 .and_then(|v| v.as_str())
4112 .unwrap_or("no-password-required")
4113 .to_string();
4114 let arn = format!(
4115 "arn:aws:elasticache:{}:{}:user:{}",
4116 self.region, self.account_id, user_id
4117 );
4118 let user = EcUser {
4119 user_id: user_id.clone(),
4120 user_name,
4121 engine,
4122 access_string,
4123 status: "active".to_string(),
4124 authentication_type,
4125 password_count: 0,
4126 arn: arn.clone(),
4127 minimum_engine_version: "6.0".to_string(),
4128 user_group_ids: Vec::new(),
4129 };
4130 let mut accounts = self.elasticache_state.write();
4131 let state = accounts.get_or_create(&self.account_id);
4132 state.users.insert(user_id.clone(), user);
4133 Ok(ProvisionResult::new(user_id).with("Arn", arn))
4134 }
4135
4136 fn delete_ec_user(&self, physical_id: &str) -> Result<(), String> {
4137 let mut accounts = self.elasticache_state.write();
4138 let state = accounts.get_or_create(&self.account_id);
4139 state.users.remove(physical_id);
4140 Ok(())
4141 }
4142
4143 fn create_ec_user_group(
4144 &self,
4145 resource: &ResourceDefinition,
4146 ) -> Result<ProvisionResult, String> {
4147 let props = &resource.properties;
4148 let user_group_id = props
4149 .get("UserGroupId")
4150 .and_then(|v| v.as_str())
4151 .unwrap_or(&resource.logical_id)
4152 .to_string();
4153 let engine = props
4154 .get("Engine")
4155 .and_then(|v| v.as_str())
4156 .unwrap_or("redis")
4157 .to_string();
4158 let user_ids: Vec<String> = props
4159 .get("UserIds")
4160 .and_then(|v| v.as_array())
4161 .map(|arr| {
4162 arr.iter()
4163 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4164 .collect()
4165 })
4166 .unwrap_or_default();
4167 let arn = format!(
4168 "arn:aws:elasticache:{}:{}:usergroup:{}",
4169 self.region, self.account_id, user_group_id
4170 );
4171 let group = EcUserGroup {
4172 user_group_id: user_group_id.clone(),
4173 engine,
4174 status: "active".to_string(),
4175 user_ids,
4176 arn: arn.clone(),
4177 minimum_engine_version: "6.0".to_string(),
4178 pending_changes: None,
4179 replication_groups: Vec::new(),
4180 };
4181 let mut accounts = self.elasticache_state.write();
4182 let state = accounts.get_or_create(&self.account_id);
4183 state.user_groups.insert(user_group_id.clone(), group);
4184 Ok(ProvisionResult::new(user_group_id).with("Arn", arn))
4185 }
4186
4187 fn delete_ec_user_group(&self, physical_id: &str) -> Result<(), String> {
4188 let mut accounts = self.elasticache_state.write();
4189 let state = accounts.get_or_create(&self.account_id);
4190 state.user_groups.remove(physical_id);
4191 Ok(())
4192 }
4193
4194 fn create_ec_cache_cluster(
4195 &self,
4196 resource: &ResourceDefinition,
4197 ) -> Result<ProvisionResult, String> {
4198 let props = &resource.properties;
4199 let id = props
4200 .get("ClusterName")
4201 .and_then(|v| v.as_str())
4202 .map(String::from)
4203 .unwrap_or_else(|| format!("cfn-cc-{}", resource.logical_id.to_lowercase()));
4204 let cache_node_type = props
4205 .get("CacheNodeType")
4206 .and_then(|v| v.as_str())
4207 .unwrap_or("cache.t4g.micro")
4208 .to_string();
4209 let engine = props
4210 .get("Engine")
4211 .and_then(|v| v.as_str())
4212 .unwrap_or("redis")
4213 .to_string();
4214 let engine_version = props
4215 .get("EngineVersion")
4216 .and_then(|v| v.as_str())
4217 .unwrap_or("7.1")
4218 .to_string();
4219 let num_cache_nodes = props
4220 .get("NumCacheNodes")
4221 .and_then(|v| v.as_i64())
4222 .map(|n| n as i32)
4223 .unwrap_or(1);
4224 let preferred_az = props
4225 .get("PreferredAvailabilityZone")
4226 .and_then(|v| v.as_str())
4227 .unwrap_or("us-east-1a")
4228 .to_string();
4229 let cache_subnet_group_name = props
4230 .get("CacheSubnetGroupName")
4231 .and_then(|v| v.as_str())
4232 .map(String::from);
4233 let auto_minor_version_upgrade = props
4234 .get("AutoMinorVersionUpgrade")
4235 .and_then(|v| v.as_bool())
4236 .unwrap_or(true);
4237 let default_port = if engine == "memcached" { 11211 } else { 6379 };
4238 let port = props
4239 .get("Port")
4240 .and_then(|v| v.as_i64())
4241 .map(|n| n as u16)
4242 .unwrap_or(default_port);
4243 let cache_parameter_group_name = props
4244 .get("CacheParameterGroupName")
4245 .and_then(|v| v.as_str())
4246 .map(String::from);
4247 let security_group_ids: Vec<String> = props
4248 .get("VpcSecurityGroupIds")
4249 .and_then(|v| v.as_array())
4250 .map(|arr| {
4251 arr.iter()
4252 .filter_map(|v| v.as_str().map(String::from))
4253 .collect()
4254 })
4255 .unwrap_or_default();
4256 let cache_security_group_names: Vec<String> = props
4257 .get("CacheSecurityGroupNames")
4258 .and_then(|v| v.as_array())
4259 .map(|arr| {
4260 arr.iter()
4261 .filter_map(|v| v.as_str().map(String::from))
4262 .collect()
4263 })
4264 .unwrap_or_default();
4265 let preferred_availability_zones: Vec<String> = props
4266 .get("PreferredAvailabilityZones")
4267 .and_then(|v| v.as_array())
4268 .map(|arr| {
4269 arr.iter()
4270 .filter_map(|v| v.as_str().map(String::from))
4271 .collect()
4272 })
4273 .unwrap_or_default();
4274 let snapshot_arns: Vec<String> = props
4275 .get("SnapshotArns")
4276 .and_then(|v| v.as_array())
4277 .map(|arr| {
4278 arr.iter()
4279 .filter_map(|v| v.as_str().map(String::from))
4280 .collect()
4281 })
4282 .unwrap_or_default();
4283 let snapshot_name = props
4284 .get("SnapshotName")
4285 .and_then(|v| v.as_str())
4286 .map(String::from);
4287 let snapshot_retention_limit = props
4288 .get("SnapshotRetentionLimit")
4289 .and_then(|v| v.as_i64())
4290 .map(|n| n as i32)
4291 .unwrap_or(0);
4292 let snapshot_window = props
4293 .get("SnapshotWindow")
4294 .and_then(|v| v.as_str())
4295 .map(String::from);
4296 let preferred_maintenance_window = props
4297 .get("PreferredMaintenanceWindow")
4298 .and_then(|v| v.as_str())
4299 .map(String::from);
4300 let notification_topic_arn = props
4301 .get("NotificationTopicArn")
4302 .and_then(|v| v.as_str())
4303 .map(String::from);
4304 let transit_encryption_enabled = props
4305 .get("TransitEncryptionEnabled")
4306 .and_then(|v| v.as_bool())
4307 .unwrap_or(false);
4308 let auth_token = props
4309 .get("AuthToken")
4310 .and_then(|v| v.as_str())
4311 .filter(|s| !s.is_empty())
4312 .map(String::from);
4313 let auth_token_enabled = auth_token.is_some();
4314 let network_type = props
4315 .get("NetworkType")
4316 .and_then(|v| v.as_str())
4317 .map(String::from)
4318 .or_else(|| Some("ipv4".to_string()));
4319 let ip_discovery = props
4320 .get("IpDiscovery")
4321 .and_then(|v| v.as_str())
4322 .map(String::from)
4323 .or_else(|| Some("ipv4".to_string()));
4324 let az_mode = props
4325 .get("AZMode")
4326 .and_then(|v| v.as_str())
4327 .map(String::from)
4328 .or_else(|| Some("single-az".to_string()));
4329 let outpost_mode = props
4330 .get("OutpostMode")
4331 .and_then(|v| v.as_str())
4332 .map(String::from);
4333 let preferred_outpost_arn = props
4334 .get("PreferredOutpostArn")
4335 .and_then(|v| v.as_str())
4336 .map(String::from);
4337
4338 let mut accounts = self.elasticache_state.write();
4339 let state = accounts.get_or_create(&self.account_id);
4340 let arn = format!(
4341 "arn:aws:elasticache:{}:{}:cluster:{}",
4342 state.region, state.account_id, id
4343 );
4344 let endpoint_address = format!("{id}.fakecloud.{}.cache.amazonaws.com", state.region);
4345 let cluster = EcCacheCluster {
4346 cache_cluster_id: id.clone(),
4347 cache_node_type,
4348 engine,
4349 engine_version,
4350 cache_cluster_status: "available".to_string(),
4351 num_cache_nodes,
4352 preferred_availability_zone: preferred_az,
4353 cache_subnet_group_name,
4354 auto_minor_version_upgrade,
4355 arn: arn.clone(),
4356 created_at: Utc::now().to_rfc3339(),
4357 endpoint_address: endpoint_address.clone(),
4358 endpoint_port: port,
4359 container_id: String::new(),
4360 host_port: 0,
4361 replication_group_id: None,
4362 cache_parameter_group_name,
4363 security_group_ids,
4364 log_delivery_configurations: Vec::new(),
4365 transit_encryption_enabled,
4366 at_rest_encryption_enabled: false,
4367 auth_token_enabled,
4368 port,
4369 preferred_maintenance_window,
4370 preferred_availability_zones,
4371 notification_topic_arn,
4372 cache_security_group_names,
4373 snapshot_arns,
4374 snapshot_name,
4375 snapshot_retention_limit,
4376 snapshot_window,
4377 outpost_mode,
4378 preferred_outpost_arn,
4379 network_type,
4380 ip_discovery,
4381 az_mode,
4382 auth_token,
4383 kms_key_id: None,
4384 transit_encryption_mode: None,
4385 data_tiering_enabled: None,
4386 cluster_mode: None,
4387 preferred_outpost_arns: Vec::new(),
4388 };
4389 state.cache_clusters.insert(id.clone(), cluster);
4390 Ok(ProvisionResult::new(id.clone())
4391 .with("Arn", arn)
4392 .with("RedisEndpoint.Address", endpoint_address.clone())
4393 .with("RedisEndpoint.Port", port.to_string())
4394 .with("ConfigurationEndpoint.Address", endpoint_address)
4395 .with("ConfigurationEndpoint.Port", port.to_string()))
4396 }
4397
4398 fn delete_ec_cache_cluster(&self, physical_id: &str) -> Result<(), String> {
4399 let mut accounts = self.elasticache_state.write();
4400 let state = accounts.get_or_create(&self.account_id);
4401 state.cache_clusters.remove(physical_id);
4402 Ok(())
4403 }
4404
4405 fn create_ec_replication_group(
4406 &self,
4407 resource: &ResourceDefinition,
4408 ) -> Result<ProvisionResult, String> {
4409 let props = &resource.properties;
4410 let id = props
4411 .get("ReplicationGroupId")
4412 .and_then(|v| v.as_str())
4413 .map(String::from)
4414 .unwrap_or_else(|| format!("cfn-rg-{}", resource.logical_id.to_lowercase()));
4415 let description = props
4416 .get("ReplicationGroupDescription")
4417 .and_then(|v| v.as_str())
4418 .unwrap_or("CFN-provisioned replication group")
4419 .to_string();
4420 let cache_node_type = props
4421 .get("CacheNodeType")
4422 .and_then(|v| v.as_str())
4423 .unwrap_or("cache.t4g.micro")
4424 .to_string();
4425 let engine = props
4426 .get("Engine")
4427 .and_then(|v| v.as_str())
4428 .unwrap_or("redis")
4429 .to_string();
4430 let engine_version = props
4431 .get("EngineVersion")
4432 .and_then(|v| v.as_str())
4433 .unwrap_or("7.1")
4434 .to_string();
4435 let num_cache_clusters = props
4436 .get("NumCacheClusters")
4437 .and_then(|v| v.as_i64())
4438 .map(|n| n as i32)
4439 .unwrap_or(1);
4440 let num_node_groups = props
4441 .get("NumNodeGroups")
4442 .and_then(|v| v.as_i64())
4443 .map(|n| n as i32)
4444 .unwrap_or(0);
4445 let replicas_per_node_group = props
4446 .get("ReplicasPerNodeGroup")
4447 .and_then(|v| v.as_i64())
4448 .map(|n| n as i32);
4449 let automatic_failover_enabled = props
4450 .get("AutomaticFailoverEnabled")
4451 .and_then(|v| v.as_bool())
4452 .unwrap_or(false);
4453 let multi_az_enabled = props
4454 .get("MultiAZEnabled")
4455 .and_then(|v| v.as_bool())
4456 .unwrap_or(false);
4457 let transit_encryption_enabled = props
4458 .get("TransitEncryptionEnabled")
4459 .and_then(|v| v.as_bool())
4460 .unwrap_or(false);
4461 let at_rest_encryption_enabled = props
4462 .get("AtRestEncryptionEnabled")
4463 .and_then(|v| v.as_bool())
4464 .unwrap_or(false);
4465 let kms_key_id = props
4466 .get("KmsKeyId")
4467 .and_then(|v| v.as_str())
4468 .map(String::from);
4469 let auth_token_enabled = props
4470 .get("AuthToken")
4471 .and_then(|v| v.as_str())
4472 .filter(|s| !s.is_empty())
4473 .is_some();
4474 let user_group_ids: Vec<String> = props
4475 .get("UserGroupIds")
4476 .and_then(|v| v.as_array())
4477 .map(|arr| {
4478 arr.iter()
4479 .filter_map(|v| v.as_str().map(String::from))
4480 .collect()
4481 })
4482 .unwrap_or_default();
4483 let snapshot_retention_limit = props
4484 .get("SnapshotRetentionLimit")
4485 .and_then(|v| v.as_i64())
4486 .map(|n| n as i32)
4487 .unwrap_or(0);
4488 let snapshot_window = props
4489 .get("SnapshotWindow")
4490 .and_then(|v| v.as_str())
4491 .unwrap_or("00:00-01:00")
4492 .to_string();
4493 let port = props
4494 .get("Port")
4495 .and_then(|v| v.as_i64())
4496 .map(|n| n as u16)
4497 .unwrap_or(6379);
4498 let cluster_enabled = num_node_groups > 1
4499 || props
4500 .get("ClusterEnabled")
4501 .and_then(|v| v.as_bool())
4502 .unwrap_or(false);
4503
4504 let mut accounts = self.elasticache_state.write();
4505 let state = accounts.get_or_create(&self.account_id);
4506 let arn = format!(
4507 "arn:aws:elasticache:{}:{}:replicationgroup:{}",
4508 state.region, state.account_id, id
4509 );
4510 let endpoint_address = format!(
4511 "{id}.fakecloud.ng.0001.{}.cache.amazonaws.com",
4512 state.region
4513 );
4514 let configuration_endpoint = if cluster_enabled {
4515 Some(format!(
4516 "{id}.fakecloud.cfg.{}.cache.amazonaws.com",
4517 state.region
4518 ))
4519 } else {
4520 None
4521 };
4522
4523 let group = EcReplicationGroup {
4524 replication_group_id: id.clone(),
4525 description,
4526 global_replication_group_id: None,
4527 global_replication_group_role: None,
4528 status: "available".to_string(),
4529 cache_node_type,
4530 engine,
4531 engine_version,
4532 num_cache_clusters,
4533 automatic_failover_enabled,
4534 endpoint_address: endpoint_address.clone(),
4535 endpoint_port: port,
4536 arn: arn.clone(),
4537 created_at: Utc::now().to_rfc3339(),
4538 container_id: String::new(),
4539 host_port: 0,
4540 member_clusters: Vec::new(),
4541 snapshot_retention_limit,
4542 snapshot_window,
4543 transit_encryption_enabled,
4544 at_rest_encryption_enabled,
4545 cluster_enabled,
4546 kms_key_id,
4547 auth_token_enabled,
4548 user_group_ids,
4549 multi_az_enabled,
4550 log_delivery_configurations: Vec::new(),
4551 data_tiering: props
4552 .get("DataTieringEnabled")
4553 .and_then(|v| v.as_bool())
4554 .map(|b| if b { "enabled" } else { "disabled" }.to_string()),
4555 ip_discovery: props
4556 .get("IpDiscovery")
4557 .and_then(|v| v.as_str())
4558 .map(String::from),
4559 network_type: props
4560 .get("NetworkType")
4561 .and_then(|v| v.as_str())
4562 .map(String::from),
4563 transit_encryption_mode: props
4564 .get("TransitEncryptionMode")
4565 .and_then(|v| v.as_str())
4566 .map(String::from),
4567 num_node_groups,
4568 configuration_endpoint_address: configuration_endpoint.clone(),
4569 configuration_endpoint_port: configuration_endpoint.as_ref().map(|_| port),
4570 replicas_per_node_group,
4571 auth_token: props
4572 .get("AuthToken")
4573 .and_then(|v| v.as_str())
4574 .filter(|s| !s.is_empty())
4575 .map(String::from),
4576 port,
4577 notification_topic_arn: props
4578 .get("NotificationTopicArn")
4579 .and_then(|v| v.as_str())
4580 .map(String::from),
4581 cluster_mode: props
4582 .get("ClusterMode")
4583 .and_then(|v| v.as_str())
4584 .map(String::from),
4585 data_tiering_enabled: props.get("DataTieringEnabled").and_then(|v| v.as_bool()),
4586 notification_topic_status: None,
4587 cache_parameter_group_name: props
4588 .get("CacheParameterGroupName")
4589 .and_then(|v| v.as_str())
4590 .map(String::from),
4591 cache_subnet_group_name: props
4592 .get("CacheSubnetGroupName")
4593 .and_then(|v| v.as_str())
4594 .map(String::from),
4595 security_group_ids: props
4596 .get("SecurityGroupIds")
4597 .and_then(|v| v.as_array())
4598 .map(|arr| {
4599 arr.iter()
4600 .filter_map(|v| v.as_str().map(String::from))
4601 .collect()
4602 })
4603 .unwrap_or_default(),
4604 preferred_maintenance_window: props
4605 .get("PreferredMaintenanceWindow")
4606 .and_then(|v| v.as_str())
4607 .map(String::from),
4608 snapshot_name: props
4609 .get("SnapshotName")
4610 .and_then(|v| v.as_str())
4611 .map(String::from),
4612 snapshot_arns: props
4613 .get("SnapshotArns")
4614 .and_then(|v| v.as_array())
4615 .map(|arr| {
4616 arr.iter()
4617 .filter_map(|v| v.as_str().map(String::from))
4618 .collect()
4619 })
4620 .unwrap_or_default(),
4621 auto_minor_version_upgrade: props
4622 .get("AutoMinorVersionUpgrade")
4623 .and_then(|v| v.as_bool())
4624 .unwrap_or(true),
4625 };
4626 state.replication_groups.insert(id.clone(), group);
4627
4628 let mut result = ProvisionResult::new(id.clone())
4629 .with("Arn", arn)
4630 .with("PrimaryEndPoint.Address", endpoint_address.clone())
4631 .with("PrimaryEndPoint.Port", port.to_string())
4632 .with("ReadEndPoint.Addresses", endpoint_address.clone())
4633 .with("ReadEndPoint.Ports", port.to_string());
4634 if let Some(cfg) = configuration_endpoint {
4635 result = result
4636 .with("ConfigurationEndPoint.Address", cfg)
4637 .with("ConfigurationEndPoint.Port", port.to_string());
4638 }
4639 Ok(result)
4640 }
4641
4642 fn delete_ec_replication_group(&self, physical_id: &str) -> Result<(), String> {
4643 let mut accounts = self.elasticache_state.write();
4644 let state = accounts.get_or_create(&self.account_id);
4645 state.replication_groups.remove(physical_id);
4646 Ok(())
4647 }
4648
4649 fn create_cf_origin_access_identity(
4656 &self,
4657 resource: &ResourceDefinition,
4658 ) -> Result<ProvisionResult, String> {
4659 let props = &resource.properties;
4660 let cfg = props
4661 .get("CloudFrontOriginAccessIdentityConfig")
4662 .ok_or("CloudFrontOriginAccessIdentityConfig is required")?;
4663 let comment = cfg
4664 .get("Comment")
4665 .and_then(|v| v.as_str())
4666 .unwrap_or("")
4667 .to_string();
4668 let caller_reference = format!("cfn-{}", resource.logical_id);
4669
4670 let id = format!(
4671 "E{}",
4672 Uuid::new_v4().simple().to_string()[..13].to_uppercase()
4673 );
4674 let etag = format!(
4675 "E{}",
4676 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
4677 );
4678 let s3_canonical_user_id = format!(
4679 "{:0<64}",
4680 Uuid::new_v4().simple().to_string().to_lowercase()
4681 );
4682
4683 let oai = StoredOriginAccessIdentity {
4684 id: id.clone(),
4685 etag,
4686 s3_canonical_user_id: s3_canonical_user_id.clone(),
4687 config: CloudFrontOriginAccessIdentityConfig {
4688 caller_reference,
4689 comment,
4690 },
4691 };
4692
4693 let mut accounts = self.cloudfront_state.write();
4694 let state = accounts.entry("000000000000");
4695 state.origin_access_identities.insert(id.clone(), oai);
4696
4697 Ok(ProvisionResult::new(id.clone())
4698 .with("Id", id)
4699 .with("S3CanonicalUserId", s3_canonical_user_id))
4700 }
4701
4702 fn delete_cf_origin_access_identity(&self, physical_id: &str) -> Result<(), String> {
4703 let mut accounts = self.cloudfront_state.write();
4704 let state = accounts.entry("000000000000");
4705 state.origin_access_identities.remove(physical_id);
4706 Ok(())
4707 }
4708
4709 fn create_cf_distribution(
4715 &self,
4716 resource: &ResourceDefinition,
4717 ) -> Result<ProvisionResult, String> {
4718 let cfg = resource
4719 .properties
4720 .get("DistributionConfig")
4721 .ok_or_else(|| "DistributionConfig is required".to_string())?;
4722
4723 let origin_entries: Vec<Origin> = cfg
4730 .get("Origins")
4731 .and_then(|v| v.as_array())
4732 .ok_or_else(|| "DistributionConfig.Origins is required".to_string())?
4733 .iter()
4734 .map(|o| {
4735 let mut patched = o.clone();
4736 if let Some(custom) = patched
4737 .get_mut("CustomOriginConfig")
4738 .and_then(|v| v.as_object_mut())
4739 {
4740 if let Some(v) = custom.remove("HTTPPort") {
4741 custom.insert("HttpPort".to_string(), v);
4742 }
4743 if let Some(v) = custom.remove("HTTPSPort") {
4744 custom.insert("HttpsPort".to_string(), v);
4745 }
4746 }
4747 serde_json::from_value::<Origin>(patched)
4748 .map_err(|e| format!("Invalid Origin entry: {e}"))
4749 })
4750 .collect::<Result<Vec<_>, _>>()?;
4751 if origin_entries.is_empty() {
4752 return Err("DistributionConfig.Origins must contain at least one origin".to_string());
4753 }
4754 let origins = Origins {
4755 quantity: origin_entries.len() as i32,
4756 items: Some(OriginItems {
4757 origin: origin_entries,
4758 }),
4759 };
4760
4761 let dcb_value = cfg
4762 .get("DefaultCacheBehavior")
4763 .ok_or_else(|| "DistributionConfig.DefaultCacheBehavior is required".to_string())?;
4764 let default_cache_behavior: DefaultCacheBehavior =
4765 serde_json::from_value(dcb_value.clone())
4766 .map_err(|e| format!("Invalid DefaultCacheBehavior: {e}"))?;
4767
4768 let comment = cfg
4769 .get("Comment")
4770 .and_then(|v| v.as_str())
4771 .unwrap_or("")
4772 .to_string();
4773 let enabled = cfg.get("Enabled").and_then(|v| v.as_bool()).unwrap_or(true);
4774 let price_class = cfg
4775 .get("PriceClass")
4776 .and_then(|v| v.as_str())
4777 .map(|s| s.to_string());
4778 let http_version = cfg
4779 .get("HttpVersion")
4780 .and_then(|v| v.as_str())
4781 .map(|s| s.to_string());
4782 let is_ipv6_enabled = cfg.get("IPV6Enabled").and_then(|v| v.as_bool());
4783 let default_root_object = cfg
4784 .get("DefaultRootObject")
4785 .and_then(|v| v.as_str())
4786 .map(|s| s.to_string());
4787 let web_acl_id = cfg
4788 .get("WebACLId")
4789 .and_then(|v| v.as_str())
4790 .map(|s| s.to_string());
4791
4792 let viewer_certificate: Option<ViewerCertificate> = cfg
4793 .get("ViewerCertificate")
4794 .map(|v| serde_json::from_value(v.clone()))
4795 .transpose()
4796 .map_err(|e| format!("Invalid ViewerCertificate: {e}"))?;
4797
4798 let caller_reference = format!("cfn-{}-{}", resource.logical_id, Uuid::new_v4().simple());
4799
4800 let mut config = DistributionConfig {
4801 caller_reference,
4802 comment,
4803 enabled,
4804 origins,
4805 default_cache_behavior,
4806 ..Default::default()
4807 };
4808 config.price_class = price_class;
4809 config.http_version = http_version;
4810 config.is_ipv6_enabled = is_ipv6_enabled;
4811 config.default_root_object = default_root_object;
4812 config.web_acl_id = web_acl_id;
4813 config.viewer_certificate = viewer_certificate;
4814
4815 let id_suffix: String = Uuid::new_v4()
4818 .simple()
4819 .to_string()
4820 .chars()
4821 .take(13)
4822 .collect::<String>()
4823 .to_uppercase();
4824 let id = format!("E{id_suffix}");
4825 let etag_suffix: String = Uuid::new_v4()
4826 .simple()
4827 .to_string()
4828 .chars()
4829 .take(7)
4830 .collect::<String>()
4831 .to_uppercase();
4832 let etag = format!("E{etag_suffix}");
4833 let domain_name = format!("{}.cloudfront.net", id.to_lowercase());
4834 let arn = format!(
4835 "arn:aws:cloudfront::{}:distribution/{}",
4836 self.account_id, id
4837 );
4838
4839 let stored = StoredDistribution {
4840 id: id.clone(),
4841 arn: arn.clone(),
4842 status: "InProgress".to_string(),
4845 last_modified_time: Utc::now(),
4846 domain_name: domain_name.clone(),
4847 in_progress_invalidation_batches: 0,
4848 etag,
4849 config,
4850 };
4851
4852 let mut accounts = self.cloudfront_state.write();
4853 let state = accounts.entry("000000000000");
4854 state.distributions.insert(id.clone(), stored);
4855 Ok(ProvisionResult::new(id.clone())
4856 .with("Id", id)
4857 .with("DomainName", domain_name)
4858 .with("Arn", arn))
4859 }
4860
4861 fn delete_cf_distribution(&self, physical_id: &str) -> Result<(), String> {
4862 let mut accounts = self.cloudfront_state.write();
4863 let state = accounts.entry("000000000000");
4864 state.distributions.remove(physical_id);
4865 Ok(())
4866 }
4867
4868 fn create_cf_origin_access_control(
4869 &self,
4870 resource: &ResourceDefinition,
4871 ) -> Result<ProvisionResult, String> {
4872 let props = &resource.properties;
4873 let cfg = props
4874 .get("OriginAccessControlConfig")
4875 .ok_or("OriginAccessControlConfig is required")?;
4876 let name = cfg
4877 .get("Name")
4878 .and_then(|v| v.as_str())
4879 .ok_or("OriginAccessControlConfig.Name is required")?
4880 .to_string();
4881 let signing_protocol = cfg
4882 .get("SigningProtocol")
4883 .and_then(|v| v.as_str())
4884 .unwrap_or("sigv4")
4885 .to_string();
4886 let signing_behavior = cfg
4887 .get("SigningBehavior")
4888 .and_then(|v| v.as_str())
4889 .unwrap_or("always")
4890 .to_string();
4891 let origin_type = cfg
4892 .get("OriginAccessControlOriginType")
4893 .and_then(|v| v.as_str())
4894 .ok_or("OriginAccessControlConfig.OriginAccessControlOriginType is required")?
4895 .to_string();
4896 let description = cfg
4897 .get("Description")
4898 .and_then(|v| v.as_str())
4899 .map(String::from);
4900
4901 let id = format!(
4902 "E{}",
4903 Uuid::new_v4().simple().to_string()[..13].to_uppercase()
4904 );
4905 let etag = format!(
4906 "E{}",
4907 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
4908 );
4909 let oac = StoredOriginAccessControl {
4910 id: id.clone(),
4911 etag,
4912 config: OriginAccessControlConfig {
4913 name,
4914 description,
4915 signing_protocol,
4916 signing_behavior,
4917 origin_access_control_origin_type: origin_type,
4918 },
4919 };
4920
4921 let mut accounts = self.cloudfront_state.write();
4922 let state = accounts.entry("000000000000");
4923 state.origin_access_controls.insert(id.clone(), oac);
4924
4925 Ok(ProvisionResult::new(id.clone()).with("Id", id))
4926 }
4927
4928 fn delete_cf_origin_access_control(&self, physical_id: &str) -> Result<(), String> {
4929 let mut accounts = self.cloudfront_state.write();
4930 let state = accounts.entry("000000000000");
4931 state.origin_access_controls.remove(physical_id);
4932 Ok(())
4933 }
4934
4935 fn create_cf_public_key(
4936 &self,
4937 resource: &ResourceDefinition,
4938 ) -> Result<ProvisionResult, String> {
4939 let props = &resource.properties;
4940 let cfg = props
4941 .get("PublicKeyConfig")
4942 .ok_or("PublicKeyConfig is required")?;
4943 let name = cfg
4944 .get("Name")
4945 .and_then(|v| v.as_str())
4946 .ok_or("PublicKeyConfig.Name is required")?
4947 .to_string();
4948 let encoded_key = cfg
4949 .get("EncodedKey")
4950 .and_then(|v| v.as_str())
4951 .ok_or("PublicKeyConfig.EncodedKey is required")?
4952 .to_string();
4953 let comment = cfg
4954 .get("Comment")
4955 .and_then(|v| v.as_str())
4956 .map(String::from);
4957 let caller_reference = cfg
4958 .get("CallerReference")
4959 .and_then(|v| v.as_str())
4960 .unwrap_or("")
4961 .to_string();
4962 let caller_reference = if caller_reference.is_empty() {
4963 format!("cfn-{}", resource.logical_id)
4964 } else {
4965 caller_reference
4966 };
4967
4968 let id = format!(
4969 "K{}",
4970 Uuid::new_v4().simple().to_string()[..13].to_uppercase()
4971 );
4972 let etag = format!(
4973 "E{}",
4974 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
4975 );
4976
4977 let pk = StoredPublicKey {
4978 id: id.clone(),
4979 etag,
4980 created_time: Utc::now(),
4981 config: PublicKeyConfig {
4982 caller_reference,
4983 name,
4984 encoded_key,
4985 comment,
4986 },
4987 };
4988
4989 let mut accounts = self.cloudfront_state.write();
4990 let state = accounts.entry("000000000000");
4991 state.public_keys.insert(id.clone(), pk);
4992
4993 Ok(ProvisionResult::new(id.clone()).with("Id", id))
4994 }
4995
4996 fn delete_cf_public_key(&self, physical_id: &str) -> Result<(), String> {
4997 let mut accounts = self.cloudfront_state.write();
4998 let state = accounts.entry("000000000000");
4999 state.public_keys.remove(physical_id);
5000 Ok(())
5001 }
5002
5003 fn create_cf_key_group(
5004 &self,
5005 resource: &ResourceDefinition,
5006 ) -> Result<ProvisionResult, String> {
5007 let props = &resource.properties;
5008 let cfg = props
5009 .get("KeyGroupConfig")
5010 .ok_or("KeyGroupConfig is required")?;
5011 let name = cfg
5012 .get("Name")
5013 .and_then(|v| v.as_str())
5014 .ok_or("KeyGroupConfig.Name is required")?
5015 .to_string();
5016 let items: Vec<String> = cfg
5017 .get("Items")
5018 .and_then(|v| v.as_array())
5019 .map(|arr| {
5020 arr.iter()
5021 .filter_map(|v| v.as_str().map(String::from))
5022 .collect()
5023 })
5024 .unwrap_or_default();
5025 let comment = cfg
5026 .get("Comment")
5027 .and_then(|v| v.as_str())
5028 .map(String::from);
5029
5030 let id = format!(
5031 "KG{}",
5032 Uuid::new_v4().simple().to_string()[..12].to_uppercase()
5033 );
5034 let etag = format!(
5035 "E{}",
5036 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
5037 );
5038
5039 let kg = StoredKeyGroup {
5040 id: id.clone(),
5041 etag,
5042 last_modified_time: Utc::now(),
5043 config: KeyGroupConfig {
5044 name,
5045 items: KeyGroupItems { public_key: items },
5046 comment,
5047 },
5048 };
5049
5050 let mut accounts = self.cloudfront_state.write();
5051 let state = accounts.entry("000000000000");
5052 state.key_groups.insert(id.clone(), kg);
5053
5054 Ok(ProvisionResult::new(id.clone()).with("Id", id))
5055 }
5056
5057 fn delete_cf_key_group(&self, physical_id: &str) -> Result<(), String> {
5058 let mut accounts = self.cloudfront_state.write();
5059 let state = accounts.entry("000000000000");
5060 state.key_groups.remove(physical_id);
5061 Ok(())
5062 }
5063
5064 fn create_cf_function(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
5065 let props = &resource.properties;
5066 let name = props
5067 .get("Name")
5068 .and_then(|v| v.as_str())
5069 .ok_or("Name is required")?
5070 .to_string();
5071 let function_code = props
5072 .get("FunctionCode")
5073 .and_then(|v| v.as_str())
5074 .ok_or("FunctionCode is required")?
5075 .to_string();
5076 let cfg = props
5077 .get("FunctionConfig")
5078 .ok_or("FunctionConfig is required")?;
5079 let runtime = cfg
5080 .get("Runtime")
5081 .and_then(|v| v.as_str())
5082 .unwrap_or("cloudfront-js-2.0")
5083 .to_string();
5084 let comment = cfg
5085 .get("Comment")
5086 .and_then(|v| v.as_str())
5087 .map(String::from);
5088
5089 let id = format!(
5090 "FN{}",
5091 Uuid::new_v4().simple().to_string()[..12].to_uppercase()
5092 );
5093 let etag = format!(
5094 "E{}",
5095 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
5096 );
5097 let function_arn =
5098 Arn::global("cloudfront", &self.account_id, &format!("function/{name}")).to_string();
5099
5100 let now = Utc::now();
5101 let func = StoredFunction {
5102 name: name.clone(),
5103 etag,
5104 status: "UNPUBLISHED".to_string(),
5105 stage: "DEVELOPMENT".to_string(),
5106 function_arn: function_arn.clone(),
5107 created_time: now,
5108 last_modified_time: now,
5109 config: FunctionConfig {
5110 comment,
5111 runtime,
5112 key_value_store_associations: None,
5113 },
5114 function_code,
5115 live_function_code: None,
5116 };
5117
5118 let mut accounts = self.cloudfront_state.write();
5119 let state = accounts.entry("000000000000");
5120 state.functions.insert(name.clone(), func);
5123
5124 Ok(ProvisionResult::new(name.clone())
5125 .with("FunctionARN", function_arn)
5126 .with("FunctionMetadata.FunctionARN", id)
5127 .with("Stage", "DEVELOPMENT"))
5128 }
5129
5130 fn delete_cf_function(&self, physical_id: &str) -> Result<(), String> {
5131 let mut accounts = self.cloudfront_state.write();
5132 let state = accounts.entry("000000000000");
5133 state.functions.remove(physical_id);
5134 Ok(())
5135 }
5136
5137 fn create_cf_cache_policy(
5138 &self,
5139 resource: &ResourceDefinition,
5140 ) -> Result<ProvisionResult, String> {
5141 let props = &resource.properties;
5142 let cfg = props
5143 .get("CachePolicyConfig")
5144 .ok_or("CachePolicyConfig is required")?;
5145 let name = cfg
5146 .get("Name")
5147 .and_then(|v| v.as_str())
5148 .ok_or("CachePolicyConfig.Name is required")?
5149 .to_string();
5150 let min_ttl = cfg
5151 .get("MinTTL")
5152 .and_then(|v| {
5153 v.as_i64()
5154 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
5155 })
5156 .unwrap_or(0);
5157 let default_ttl = cfg.get("DefaultTTL").and_then(|v| {
5158 v.as_i64()
5159 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
5160 });
5161 let max_ttl = cfg.get("MaxTTL").and_then(|v| {
5162 v.as_i64()
5163 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
5164 });
5165 let comment = cfg
5166 .get("Comment")
5167 .and_then(|v| v.as_str())
5168 .map(String::from);
5169
5170 let id = format!(
5171 "CP{}",
5172 Uuid::new_v4().simple().to_string()[..12].to_uppercase()
5173 );
5174 let etag = format!(
5175 "E{}",
5176 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
5177 );
5178
5179 let cache_policy = StoredCachePolicy {
5180 id: id.clone(),
5181 etag,
5182 last_modified_time: Utc::now(),
5183 config: CachePolicyConfig {
5184 comment,
5185 name,
5186 default_ttl,
5187 max_ttl,
5188 min_ttl,
5189 parameters_in_cache_key_and_forwarded_to_origin: None,
5190 },
5191 policy_type: "custom".to_string(),
5192 };
5193
5194 let mut accounts = self.cloudfront_state.write();
5195 let state = accounts.entry("000000000000");
5196 state.cache_policies.insert(id.clone(), cache_policy);
5197
5198 Ok(ProvisionResult::new(id.clone()).with("Id", id))
5199 }
5200
5201 fn delete_cf_cache_policy(&self, physical_id: &str) -> Result<(), String> {
5202 let mut accounts = self.cloudfront_state.write();
5203 let state = accounts.entry("000000000000");
5204 state.cache_policies.remove(physical_id);
5205 Ok(())
5206 }
5207
5208 fn create_cf_origin_request_policy(
5209 &self,
5210 resource: &ResourceDefinition,
5211 ) -> Result<ProvisionResult, String> {
5212 let props = &resource.properties;
5213 let cfg = props
5214 .get("OriginRequestPolicyConfig")
5215 .ok_or("OriginRequestPolicyConfig is required")?;
5216 let name = cfg
5217 .get("Name")
5218 .and_then(|v| v.as_str())
5219 .ok_or("OriginRequestPolicyConfig.Name is required")?
5220 .to_string();
5221 let header_behavior = cfg
5222 .get("HeadersConfig")
5223 .and_then(|v| v.get("HeaderBehavior"))
5224 .and_then(|v| v.as_str())
5225 .unwrap_or("none")
5226 .to_string();
5227 let cookie_behavior = cfg
5228 .get("CookiesConfig")
5229 .and_then(|v| v.get("CookieBehavior"))
5230 .and_then(|v| v.as_str())
5231 .unwrap_or("none")
5232 .to_string();
5233 let query_string_behavior = cfg
5234 .get("QueryStringsConfig")
5235 .and_then(|v| v.get("QueryStringBehavior"))
5236 .and_then(|v| v.as_str())
5237 .unwrap_or("none")
5238 .to_string();
5239 let comment = cfg
5240 .get("Comment")
5241 .and_then(|v| v.as_str())
5242 .map(String::from);
5243
5244 let id = format!(
5245 "ORP{}",
5246 Uuid::new_v4().simple().to_string()[..11].to_uppercase()
5247 );
5248 let etag = format!(
5249 "E{}",
5250 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
5251 );
5252
5253 let policy = StoredOriginRequestPolicy {
5254 id: id.clone(),
5255 etag,
5256 last_modified_time: Utc::now(),
5257 config: OriginRequestPolicyConfig {
5258 comment,
5259 name,
5260 headers_config: OriginRequestPolicyHeadersConfig {
5261 header_behavior,
5262 headers: None,
5263 },
5264 cookies_config: OriginRequestPolicyCookiesConfig {
5265 cookie_behavior,
5266 cookies: None,
5267 },
5268 query_strings_config: OriginRequestPolicyQueryStringsConfig {
5269 query_string_behavior,
5270 query_strings: None,
5271 },
5272 },
5273 policy_type: "custom".to_string(),
5274 };
5275
5276 let mut accounts = self.cloudfront_state.write();
5277 let state = accounts.entry("000000000000");
5278 state.origin_request_policies.insert(id.clone(), policy);
5279
5280 Ok(ProvisionResult::new(id.clone()).with("Id", id))
5281 }
5282
5283 fn delete_cf_origin_request_policy(&self, physical_id: &str) -> Result<(), String> {
5284 let mut accounts = self.cloudfront_state.write();
5285 let state = accounts.entry("000000000000");
5286 state.origin_request_policies.remove(physical_id);
5287 Ok(())
5288 }
5289
5290 fn create_cf_response_headers_policy(
5291 &self,
5292 resource: &ResourceDefinition,
5293 ) -> Result<ProvisionResult, String> {
5294 let props = &resource.properties;
5295 let cfg = props
5296 .get("ResponseHeadersPolicyConfig")
5297 .ok_or("ResponseHeadersPolicyConfig is required")?;
5298 let name = cfg
5299 .get("Name")
5300 .and_then(|v| v.as_str())
5301 .ok_or("ResponseHeadersPolicyConfig.Name is required")?
5302 .to_string();
5303 let comment = cfg
5304 .get("Comment")
5305 .and_then(|v| v.as_str())
5306 .map(String::from);
5307
5308 let id = format!(
5309 "RHP{}",
5310 Uuid::new_v4().simple().to_string()[..11].to_uppercase()
5311 );
5312 let etag = format!(
5313 "E{}",
5314 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
5315 );
5316
5317 let policy = StoredResponseHeadersPolicy {
5318 id: id.clone(),
5319 etag,
5320 last_modified_time: Utc::now(),
5321 config: ResponseHeadersPolicyConfig {
5322 comment,
5323 name,
5324 cors_config: None,
5325 security_headers_config: None,
5326 server_timing_headers_config: None,
5327 custom_headers_config: None,
5328 remove_headers_config: None,
5329 },
5330 policy_type: "custom".to_string(),
5331 };
5332
5333 let mut accounts = self.cloudfront_state.write();
5334 let state = accounts.entry("000000000000");
5335 state.response_headers_policies.insert(id.clone(), policy);
5336
5337 Ok(ProvisionResult::new(id.clone()).with("Id", id))
5338 }
5339
5340 fn delete_cf_response_headers_policy(&self, physical_id: &str) -> Result<(), String> {
5341 let mut accounts = self.cloudfront_state.write();
5342 let state = accounts.entry("000000000000");
5343 state.response_headers_policies.remove(physical_id);
5344 Ok(())
5345 }
5346
5347 fn parse_athena_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
5348 let mut out = BTreeMap::new();
5349 let Some(arr) = value.and_then(|v| v.as_array()) else {
5350 return out;
5351 };
5352 for tag in arr {
5353 if let (Some(k), Some(v)) = (
5354 tag.get("Key").and_then(|v| v.as_str()),
5355 tag.get("Value").and_then(|v| v.as_str()),
5356 ) {
5357 out.insert(k.to_string(), v.to_string());
5358 }
5359 }
5360 out
5361 }
5362
5363 fn fetch_template_from_url(&self, url: &str) -> Result<String, String> {
5364 if let Some(rest) = url.strip_prefix("s3://") {
5365 let parts: Vec<&str> = rest.splitn(2, '/').collect();
5366 if parts.len() != 2 {
5367 return Err("Invalid s3:// URL".to_string());
5368 }
5369 return self.fetch_s3_template(parts[0], parts[1]);
5370 }
5371
5372 if let Some(rest) = url.strip_prefix("https://s3.amazonaws.com/") {
5373 let parts: Vec<&str> = rest.splitn(2, '/').collect();
5374 if parts.len() != 2 {
5375 return Err("Invalid S3 HTTPS URL".to_string());
5376 }
5377 return self.fetch_s3_template(parts[0], parts[1]);
5378 }
5379
5380 if let Some(host_rest) = url.strip_prefix("https://") {
5381 if let Some(slash_pos) = host_rest.find('/') {
5382 let host = &host_rest[..slash_pos];
5383 let key = &host_rest[slash_pos + 1..];
5384 if let Some(bucket) = host.strip_suffix(".s3.amazonaws.com") {
5385 return self.fetch_s3_template(bucket, key);
5386 }
5387 if host.contains(".s3.") && host.ends_with(".amazonaws.com") {
5388 let bucket = host.split(".s3.").next().unwrap_or("");
5389 if !bucket.is_empty() {
5390 return self.fetch_s3_template(bucket, key);
5391 }
5392 }
5393 }
5394 }
5395
5396 Err(format!("Unsupported TemplateURL: {url}"))
5397 }
5398
5399 fn fetch_s3_template(&self, bucket: &str, key: &str) -> Result<String, String> {
5400 let mut s3_accounts = self.s3_state.write();
5401 let s3_state = s3_accounts.get_or_create(&self.account_id);
5402 let bucket_obj = s3_state
5403 .buckets
5404 .get(bucket)
5405 .ok_or_else(|| format!("S3 bucket not found: {bucket}"))?;
5406 let obj = bucket_obj
5407 .objects
5408 .get(key)
5409 .ok_or_else(|| format!("S3 object not found: {bucket}/{key}"))?;
5410 let bytes = s3_state
5411 .read_body(&obj.body)
5412 .map_err(|e| format!("Failed to read S3 object body: {e}"))?;
5413 String::from_utf8(bytes.to_vec()).map_err(|e| format!("S3 object is not valid UTF-8: {e}"))
5414 }
5415}
5416
5417fn generate_secret_string_payload(gen: &serde_json::Value) -> Result<String, String> {
5426 let length = gen
5427 .get("PasswordLength")
5428 .and_then(|v| v.as_i64())
5429 .unwrap_or(32) as usize;
5430 let exclude_lowercase = gen
5431 .get("ExcludeLowercase")
5432 .and_then(|v| v.as_bool())
5433 .unwrap_or(false);
5434 let exclude_uppercase = gen
5435 .get("ExcludeUppercase")
5436 .and_then(|v| v.as_bool())
5437 .unwrap_or(false);
5438 let exclude_numbers = gen
5439 .get("ExcludeNumbers")
5440 .and_then(|v| v.as_bool())
5441 .unwrap_or(false);
5442 let exclude_punctuation = gen
5443 .get("ExcludePunctuation")
5444 .and_then(|v| v.as_bool())
5445 .unwrap_or(false);
5446 let include_space = gen
5447 .get("IncludeSpace")
5448 .and_then(|v| v.as_bool())
5449 .unwrap_or(false);
5450 let exclude_chars = gen
5451 .get("ExcludeCharacters")
5452 .and_then(|v| v.as_str())
5453 .unwrap_or("")
5454 .to_string();
5455
5456 let lowercase = "abcdefghijklmnopqrstuvwxyz";
5457 let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
5458 let digits = "0123456789";
5459 let punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
5460
5461 let mut pool = String::new();
5462 if !exclude_lowercase {
5463 pool.extend(lowercase.chars().filter(|c| !exclude_chars.contains(*c)));
5464 }
5465 if !exclude_uppercase {
5466 pool.extend(uppercase.chars().filter(|c| !exclude_chars.contains(*c)));
5467 }
5468 if !exclude_numbers {
5469 pool.extend(digits.chars().filter(|c| !exclude_chars.contains(*c)));
5470 }
5471 if !exclude_punctuation {
5472 pool.extend(punctuation.chars().filter(|c| !exclude_chars.contains(*c)));
5473 }
5474 if include_space && !exclude_chars.contains(' ') {
5475 pool.push(' ');
5476 }
5477 if pool.is_empty() {
5478 return Err("GenerateSecretString character pool is empty".to_string());
5479 }
5480
5481 let pool_chars: Vec<char> = pool.chars().collect();
5482 let mut password = String::with_capacity(length);
5483 let mut counter: u64 = std::time::SystemTime::now()
5484 .duration_since(std::time::UNIX_EPOCH)
5485 .map(|d| d.as_nanos() as u64)
5486 .unwrap_or(0);
5487 while password.len() < length {
5488 counter = counter.wrapping_add(0x9E3779B97F4A7C15);
5491 let mut z = counter;
5492 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
5493 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
5494 z ^= z >> 31;
5495 let idx = (z as usize) % pool_chars.len();
5496 password.push(pool_chars[idx]);
5497 }
5498
5499 let template = gen.get("SecretStringTemplate").and_then(|v| v.as_str());
5500 let key = gen.get("GenerateStringKey").and_then(|v| v.as_str());
5501 match (template, key) {
5502 (Some(tmpl), Some(k)) => {
5503 let mut value: serde_json::Value = serde_json::from_str(tmpl)
5504 .map_err(|e| format!("SecretStringTemplate is not valid JSON: {e}"))?;
5505 if let Some(obj) = value.as_object_mut() {
5506 obj.insert(k.to_string(), serde_json::Value::String(password));
5507 Ok(value.to_string())
5508 } else {
5509 Err("SecretStringTemplate must be a JSON object".to_string())
5510 }
5511 }
5512 _ => Ok(password),
5513 }
5514}
5515
5516fn parse_ses_receipt_action(value: &serde_json::Value) -> Option<SesReceiptAction> {
5517 let obj = value.as_object()?;
5518 if let Some(s3) = obj.get("S3Action").and_then(|v| v.as_object()) {
5519 let bucket_name = s3.get("BucketName").and_then(|v| v.as_str())?.to_string();
5520 return Some(SesReceiptAction::S3 {
5521 bucket_name,
5522 object_key_prefix: s3
5523 .get("ObjectKeyPrefix")
5524 .and_then(|v| v.as_str())
5525 .map(String::from),
5526 topic_arn: s3
5527 .get("TopicArn")
5528 .and_then(|v| v.as_str())
5529 .map(String::from),
5530 kms_key_arn: s3
5531 .get("KmsKeyArn")
5532 .and_then(|v| v.as_str())
5533 .map(String::from),
5534 });
5535 }
5536 if let Some(sns) = obj.get("SNSAction").and_then(|v| v.as_object()) {
5537 return Some(SesReceiptAction::Sns {
5538 topic_arn: sns.get("TopicArn").and_then(|v| v.as_str())?.to_string(),
5539 encoding: sns
5540 .get("Encoding")
5541 .and_then(|v| v.as_str())
5542 .map(String::from),
5543 });
5544 }
5545 if let Some(la) = obj.get("LambdaAction").and_then(|v| v.as_object()) {
5546 return Some(SesReceiptAction::Lambda {
5547 function_arn: la.get("FunctionArn").and_then(|v| v.as_str())?.to_string(),
5548 invocation_type: la
5549 .get("InvocationType")
5550 .and_then(|v| v.as_str())
5551 .map(String::from),
5552 topic_arn: la
5553 .get("TopicArn")
5554 .and_then(|v| v.as_str())
5555 .map(String::from),
5556 });
5557 }
5558 if let Some(b) = obj.get("BounceAction").and_then(|v| v.as_object()) {
5559 return Some(SesReceiptAction::Bounce {
5560 smtp_reply_code: b
5561 .get("SmtpReplyCode")
5562 .and_then(|v| v.as_str())
5563 .unwrap_or("550")
5564 .to_string(),
5565 message: b
5566 .get("Message")
5567 .and_then(|v| v.as_str())
5568 .unwrap_or("")
5569 .to_string(),
5570 sender: b
5571 .get("Sender")
5572 .and_then(|v| v.as_str())
5573 .unwrap_or("")
5574 .to_string(),
5575 status_code: b
5576 .get("StatusCode")
5577 .and_then(|v| v.as_str())
5578 .map(String::from),
5579 topic_arn: b.get("TopicArn").and_then(|v| v.as_str()).map(String::from),
5580 });
5581 }
5582 if let Some(ah) = obj.get("AddHeaderAction").and_then(|v| v.as_object()) {
5583 return Some(SesReceiptAction::AddHeader {
5584 header_name: ah.get("HeaderName").and_then(|v| v.as_str())?.to_string(),
5585 header_value: ah.get("HeaderValue").and_then(|v| v.as_str())?.to_string(),
5586 });
5587 }
5588 if let Some(s) = obj.get("StopAction").and_then(|v| v.as_object()) {
5589 return Some(SesReceiptAction::Stop {
5590 scope: s
5591 .get("Scope")
5592 .and_then(|v| v.as_str())
5593 .unwrap_or("RuleSet")
5594 .to_string(),
5595 topic_arn: s.get("TopicArn").and_then(|v| v.as_str()).map(String::from),
5596 });
5597 }
5598 None
5599}
5600
5601fn make_apigwv2_id(n: usize) -> String {
5605 let s = uuid::Uuid::new_v4().simple().to_string();
5606 s[..n.min(s.len())].to_string()
5607}
5608
5609fn cfn_as_i64(v: &serde_json::Value) -> Option<i64> {
5619 if let Some(n) = v.as_i64() {
5620 return Some(n);
5621 }
5622 v.as_str().and_then(|s| s.parse::<i64>().ok())
5623}
5624
5625fn lowercase_first_keys(value: serde_json::Value) -> serde_json::Value {
5626 match value {
5627 serde_json::Value::Object(map) => {
5628 let mut out = serde_json::Map::new();
5629 for (k, v) in map {
5630 let new_key = if let Some(first) = k.chars().next() {
5631 let mut s = String::with_capacity(k.len());
5632 s.extend(first.to_lowercase());
5633 s.push_str(&k[first.len_utf8()..]);
5634 s
5635 } else {
5636 k
5637 };
5638 out.insert(new_key, lowercase_first_keys(v));
5639 }
5640 serde_json::Value::Object(out)
5641 }
5642 serde_json::Value::Array(arr) => {
5643 serde_json::Value::Array(arr.into_iter().map(lowercase_first_keys).collect())
5644 }
5645 other => other,
5646 }
5647}
5648
5649fn synth_acm_domain_validation(
5656 domain_name: &str,
5657 sans: &[String],
5658 validation_method: &str,
5659) -> Vec<AcmDomainValidation> {
5660 let mut all = vec![domain_name.to_string()];
5661 for s in sans {
5662 if !all.contains(s) {
5663 all.push(s.clone());
5664 }
5665 }
5666 all.into_iter()
5667 .map(|name| AcmDomainValidation {
5668 domain_name: name.clone(),
5669 validation_status: "SUCCESS".to_string(),
5670 validation_method: validation_method.to_string(),
5671 resource_record_name: Some(format!("_amzn-validations.{name}.")),
5672 resource_record_type: Some("CNAME".to_string()),
5673 resource_record_value: Some(format!("{}.acm-validations.aws.", Uuid::new_v4())),
5674 })
5675 .collect()
5676}
5677
5678fn parse_acm_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
5680 let mut out = BTreeMap::new();
5681 if let Some(arr) = value.and_then(|v| v.as_array()) {
5682 for t in arr {
5683 if let (Some(k), Some(v)) = (
5684 t.get("Key").and_then(|v| v.as_str()),
5685 t.get("Value").and_then(|v| v.as_str()),
5686 ) {
5687 out.insert(k.to_string(), v.to_string());
5688 }
5689 }
5690 }
5691 out
5692}
5693
5694fn parse_ecs_tags(value: Option<&serde_json::Value>) -> Vec<EcsTagEntry> {
5696 let Some(arr) = value.and_then(|v| v.as_array()) else {
5697 return Vec::new();
5698 };
5699 arr.iter()
5700 .filter_map(|t| {
5701 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
5702 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
5703 Some(EcsTagEntry { key, value })
5704 })
5705 .collect()
5706}
5707
5708fn parse_ecs_cluster_name(input: &str) -> String {
5711 if let Some(after) = input.split(":cluster/").nth(1) {
5712 return after.to_string();
5713 }
5714 input.to_string()
5715}
5716
5717fn parse_td_arn(input: &str) -> (String, i32) {
5721 let suffix = input.rsplit('/').next().unwrap_or(input);
5722 if let Some((family, rev)) = suffix.split_once(':') {
5723 if let Ok(revision) = rev.parse::<i32>() {
5724 return (family.to_string(), revision);
5725 }
5726 }
5727 (input.to_string(), 1)
5728}
5729
5730fn parse_service_arn(input: &str) -> Option<(String, String)> {
5733 let after = input.split(":service/").nth(1)?;
5734 let mut parts = after.splitn(2, '/');
5735 let cluster = parts.next()?.to_string();
5736 let service = parts.next()?.to_string();
5737 Some((cluster, service))
5738}
5739
5740fn parse_rds_tags(value: Option<&serde_json::Value>) -> Vec<RdsTag> {
5742 let Some(arr) = value.and_then(|v| v.as_array()) else {
5743 return Vec::new();
5744 };
5745 arr.iter()
5746 .filter_map(|t| {
5747 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
5748 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
5749 Some(RdsTag { key, value })
5750 })
5751 .collect()
5752}
5753
5754fn rds_extras_mut<'a>(
5758 state: &'a mut fakecloud_rds::RdsState,
5759 category: &str,
5760) -> &'a mut BTreeMap<String, serde_json::Value> {
5761 state.extras.entry(category.to_string()).or_default()
5762}
5763
5764fn parse_cognito_string_array(value: Option<&serde_json::Value>) -> Vec<String> {
5768 value
5769 .and_then(|v| v.as_array())
5770 .map(|arr| {
5771 arr.iter()
5772 .filter_map(|v| v.as_str().map(|s| s.to_string()))
5773 .collect()
5774 })
5775 .unwrap_or_default()
5776}
5777
5778fn parse_cognito_password_policy(value: Option<&serde_json::Value>) -> PasswordPolicy {
5779 let Some(inner) = value
5780 .and_then(|v| v.get("PasswordPolicy"))
5781 .and_then(|v| v.as_object())
5782 else {
5783 return PasswordPolicy::default();
5784 };
5785 let mut p = PasswordPolicy::default();
5786 if let Some(n) = inner.get("MinimumLength").and_then(|v| v.as_i64()) {
5787 p.minimum_length = n;
5788 }
5789 if let Some(b) = inner.get("RequireUppercase").and_then(|v| v.as_bool()) {
5790 p.require_uppercase = b;
5791 }
5792 if let Some(b) = inner.get("RequireLowercase").and_then(|v| v.as_bool()) {
5793 p.require_lowercase = b;
5794 }
5795 if let Some(b) = inner.get("RequireNumbers").and_then(|v| v.as_bool()) {
5796 p.require_numbers = b;
5797 }
5798 if let Some(b) = inner.get("RequireSymbols").and_then(|v| v.as_bool()) {
5799 p.require_symbols = b;
5800 }
5801 if let Some(n) = inner
5802 .get("TemporaryPasswordValidityDays")
5803 .and_then(|v| v.as_i64())
5804 {
5805 p.temporary_password_validity_days = n;
5806 }
5807 p
5808}
5809
5810fn parse_cognito_schema_attribute(value: &serde_json::Value) -> Option<SchemaAttribute> {
5811 let name = value.get("Name").and_then(|v| v.as_str())?.to_string();
5812 Some(SchemaAttribute {
5813 name,
5814 attribute_data_type: value
5815 .get("AttributeDataType")
5816 .and_then(|v| v.as_str())
5817 .unwrap_or("String")
5818 .to_string(),
5819 developer_only_attribute: value
5820 .get("DeveloperOnlyAttribute")
5821 .and_then(|v| v.as_bool())
5822 .unwrap_or(false),
5823 mutable: value
5824 .get("Mutable")
5825 .and_then(|v| v.as_bool())
5826 .unwrap_or(true),
5827 required: value
5828 .get("Required")
5829 .and_then(|v| v.as_bool())
5830 .unwrap_or(false),
5831 string_attribute_constraints: None,
5832 number_attribute_constraints: None,
5833 })
5834}
5835
5836fn parse_cognito_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
5837 let mut out = BTreeMap::new();
5838 if let Some(obj) = value.and_then(|v| v.as_object()) {
5839 for (k, v) in obj {
5840 if let Some(s) = v.as_str() {
5841 out.insert(k.clone(), s.to_string());
5842 }
5843 }
5844 }
5845 out
5846}
5847
5848fn parse_cognito_email_configuration(
5849 value: Option<&serde_json::Value>,
5850) -> Option<EmailConfiguration> {
5851 let inner = value?.as_object()?;
5852 Some(EmailConfiguration {
5853 source_arn: inner
5854 .get("SourceArn")
5855 .and_then(|v| v.as_str())
5856 .map(|s| s.to_string()),
5857 reply_to_email_address: inner
5858 .get("ReplyToEmailAddress")
5859 .and_then(|v| v.as_str())
5860 .map(|s| s.to_string()),
5861 email_sending_account: inner
5862 .get("EmailSendingAccount")
5863 .and_then(|v| v.as_str())
5864 .map(|s| s.to_string()),
5865 from_email_address: inner
5866 .get("From")
5867 .and_then(|v| v.as_str())
5868 .map(|s| s.to_string()),
5869 configuration_set: inner
5870 .get("ConfigurationSet")
5871 .and_then(|v| v.as_str())
5872 .map(|s| s.to_string()),
5873 })
5874}
5875
5876fn parse_cognito_sms_configuration(value: Option<&serde_json::Value>) -> Option<SmsConfiguration> {
5877 let inner = value?.as_object()?;
5878 Some(SmsConfiguration {
5879 sns_caller_arn: inner
5880 .get("SnsCallerArn")
5881 .and_then(|v| v.as_str())
5882 .map(|s| s.to_string()),
5883 external_id: inner
5884 .get("ExternalId")
5885 .and_then(|v| v.as_str())
5886 .map(|s| s.to_string()),
5887 sns_region: inner
5888 .get("SnsRegion")
5889 .and_then(|v| v.as_str())
5890 .map(|s| s.to_string()),
5891 })
5892}
5893
5894fn parse_cognito_admin_create_user_config(
5895 value: Option<&serde_json::Value>,
5896) -> Option<AdminCreateUserConfig> {
5897 let inner = value?.as_object()?;
5898 Some(AdminCreateUserConfig {
5899 allow_admin_create_user_only: inner
5900 .get("AllowAdminCreateUserOnly")
5901 .and_then(|v| v.as_bool()),
5902 invite_message_template: None,
5903 unused_account_validity_days: inner
5904 .get("UnusedAccountValidityDays")
5905 .and_then(|v| v.as_i64()),
5906 })
5907}
5908
5909fn parse_cognito_account_recovery(
5910 value: Option<&serde_json::Value>,
5911) -> Option<AccountRecoverySetting> {
5912 let arr = value?.get("RecoveryMechanisms")?.as_array()?;
5913 Some(AccountRecoverySetting {
5914 recovery_mechanisms: arr
5915 .iter()
5916 .filter_map(|m| {
5917 let name = m.get("Name").and_then(|v| v.as_str())?.to_string();
5918 let priority = m.get("Priority").and_then(|v| v.as_i64()).unwrap_or(1);
5919 Some(RecoveryOption { name, priority })
5920 })
5921 .collect(),
5922 })
5923}
5924
5925fn parse_firehose_s3_destination(value: &serde_json::Value) -> Result<S3Destination, String> {
5926 let role_arn = value
5927 .get("RoleARN")
5928 .and_then(|v| v.as_str())
5929 .ok_or("S3 destination requires RoleARN")?
5930 .to_string();
5931 let bucket_arn = value
5932 .get("BucketARN")
5933 .and_then(|v| v.as_str())
5934 .ok_or("S3 destination requires BucketARN")?
5935 .to_string();
5936 let prefix = value
5937 .get("Prefix")
5938 .and_then(|v| v.as_str())
5939 .map(|s| s.to_string());
5940 let error_output_prefix = value
5941 .get("ErrorOutputPrefix")
5942 .and_then(|v| v.as_str())
5943 .map(|s| s.to_string());
5944 let mut buffering_size_mb = None;
5945 let mut buffering_interval_seconds = None;
5946 if let Some(hints) = value.get("BufferingHints") {
5947 buffering_size_mb = hints.get("SizeInMBs").and_then(|v| v.as_i64());
5948 buffering_interval_seconds = hints.get("IntervalInSeconds").and_then(|v| v.as_i64());
5949 }
5950 let compression_format = value
5951 .get("CompressionFormat")
5952 .and_then(|v| v.as_str())
5953 .map(|s| s.to_string());
5954
5955 Ok(S3Destination {
5956 destination_id: "destination-1".to_string(),
5957 role_arn,
5958 bucket_arn,
5959 prefix,
5960 error_output_prefix,
5961 buffering_size_mb,
5962 buffering_interval_seconds,
5963 compression_format,
5964 processing_configuration: None,
5965 data_format_conversion_configuration: None,
5966 cloudwatch_logging_options: None,
5967 custom_time_zone: None,
5968 s3_backup_mode: None,
5969 file_extension: None,
5970 })
5971}
5972
5973#[cfg(test)]
5974mod tests {
5975 use super::*;
5976 use parking_lot::RwLock;
5977
5978 fn make_provisioner() -> ResourceProvisioner {
5979 ResourceProvisioner {
5980 sqs_state: Arc::new(RwLock::new(
5981 fakecloud_core::multi_account::MultiAccountState::new(
5982 "123456789012",
5983 "us-east-1",
5984 "http://localhost:4566",
5985 ),
5986 )),
5987 sns_state: Arc::new(RwLock::new(
5988 fakecloud_core::multi_account::MultiAccountState::new(
5989 "123456789012",
5990 "us-east-1",
5991 "http://localhost:4566",
5992 ),
5993 )),
5994 ssm_state: Arc::new(RwLock::new(
5995 fakecloud_core::multi_account::MultiAccountState::new(
5996 "123456789012",
5997 "us-east-1",
5998 "http://localhost:4566",
5999 ),
6000 )),
6001 iam_state: Arc::new(RwLock::new(
6002 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "http://localhost:4566"),
6003 )),
6004 s3_state: Arc::new(RwLock::new(fakecloud_core::multi_account::MultiAccountState::new(
6005 "123456789012",
6006 "us-east-1", "",
6007 ))),
6008 eventbridge_state: Arc::new(RwLock::new(
6009 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6010 )),
6011 dynamodb_state: Arc::new(RwLock::new(fakecloud_core::multi_account::MultiAccountState::new(
6012 "123456789012",
6013 "us-east-1", "",
6014 ))),
6015 logs_state: Arc::new(RwLock::new(
6016 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6017 )),
6018 lambda_state: Arc::new(RwLock::new(
6019 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6020 )),
6021 secretsmanager_state: Arc::new(RwLock::new(
6022 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6023 )),
6024 kinesis_state: Arc::new(RwLock::new(
6025 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6026 )),
6027 kms_state: Arc::new(RwLock::new(
6028 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6029 )),
6030 ecr_state: Arc::new(RwLock::new(
6031 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6032 )),
6033 cloudwatch_state: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
6034 elbv2_state: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
6035 organizations_state: Arc::new(RwLock::new(None)),
6036 cognito_state: Arc::new(RwLock::new(
6037 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6038 )),
6039 rds_state: Arc::new(RwLock::new(
6040 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6041 )),
6042 ec2_state: Arc::new(RwLock::new(
6043 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6044 )),
6045 autoscaling_state: Arc::new(RwLock::new(
6046 fakecloud_autoscaling::AutoScalingAccounts::new(),
6047 )),
6048 ecs_state: Arc::new(RwLock::new(
6049 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6050 )),
6051 acm_state: Arc::new(RwLock::new(fakecloud_acm::AcmAccounts::new())),
6052 elasticache_state: Arc::new(RwLock::new(
6053 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6054 )),
6055 route53_state: Arc::new(RwLock::new(fakecloud_route53::Route53Accounts::new())),
6056 cloudfront_state: Arc::new(RwLock::new(
6057 fakecloud_cloudfront::CloudFrontAccounts::new(),
6058 )),
6059 cloudformation_state: Arc::new(RwLock::new(
6060 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
6061 )),
6062 stepfunctions_state: Arc::new(RwLock::new(
6063 fakecloud_core::multi_account::MultiAccountState::new(
6064 "123456789012",
6065 "us-east-1",
6066 "",
6067 ),
6068 )),
6069 wafv2_state: Arc::new(RwLock::new(fakecloud_wafv2::Wafv2Accounts::default())),
6070 apigateway_state: Arc::new(RwLock::new(
6071 fakecloud_core::multi_account::MultiAccountState::new(
6072 "123456789012",
6073 "us-east-1",
6074 "",
6075 ),
6076 )),
6077 apigatewayv2_state: Arc::new(RwLock::new(
6078 fakecloud_core::multi_account::MultiAccountState::new(
6079 "123456789012",
6080 "us-east-1",
6081 "",
6082 ),
6083 )),
6084 ses_state: Arc::new(RwLock::new(
6085 fakecloud_core::multi_account::MultiAccountState::new(
6086 "123456789012",
6087 "us-east-1",
6088 "",
6089 ),
6090 )),
6091 app_autoscaling_state: Arc::new(parking_lot::RwLock::new(
6092 fakecloud_application_autoscaling::ApplicationAutoScalingAccounts::new(),
6093 )),
6094 athena_state: Arc::new(parking_lot::RwLock::new(
6095 fakecloud_athena::AthenaAccounts::new(),
6096 )),
6097 firehose_state: Arc::new(parking_lot::RwLock::new(
6098 fakecloud_firehose::FirehoseAccounts::new(),
6099 )),
6100 glue_state: Arc::new(parking_lot::RwLock::new(
6101 fakecloud_glue::GlueAccounts::new(),
6102 )),
6103 delivery: Arc::new(DeliveryBus::new()),
6104 lambda_runtime: None,
6105 s3_store: Arc::new(fakecloud_persistence::s3::MemoryS3Store::new()),
6106 account_id: "123456789012".to_string(),
6107 region: "us-east-1".to_string(),
6108 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test/00000000-0000-0000-0000-000000000000".to_string(),
6109 }
6110 }
6111
6112 fn make_resource(
6113 resource_type: &str,
6114 logical_id: &str,
6115 props: serde_json::Value,
6116 ) -> ResourceDefinition {
6117 ResourceDefinition {
6118 logical_id: logical_id.to_string(),
6119 resource_type: resource_type.to_string(),
6120 properties: props,
6121 }
6122 }
6123
6124 #[test]
6125 fn update_stack_reconciles_iam_role_policies() {
6126 let prov = make_provisioner();
6127 let created = prov
6128 .create_resource(&make_resource(
6129 "AWS::IAM::Role",
6130 "R",
6131 serde_json::json!({
6132 "RoleName": "r1",
6133 "AssumeRolePolicyDocument": {"Version": "2012-10-17"},
6134 "ManagedPolicyArns": ["arn:aws:iam::aws:policy/ReadOnlyAccess"],
6135 }),
6136 ))
6137 .expect("role provisions");
6138 prov.update_resource(
6139 &created,
6140 &make_resource(
6141 "AWS::IAM::Role",
6142 "R",
6143 serde_json::json!({
6144 "RoleName": "r1",
6145 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []},
6146 "Description": "updated",
6147 "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AdministratorAccess"],
6148 "Policies": [{"PolicyName": "inline1", "PolicyDocument": {"k": "v"}}],
6149 }),
6150 ),
6151 )
6152 .expect("update succeeds")
6153 .expect("IAM::Role is updatable");
6154 let iam = prov.iam_state.read();
6155 let acct = iam.get("123456789012").unwrap();
6156 let role = acct.roles.get("r1").unwrap();
6157 assert_eq!(role.description.as_deref(), Some("updated"));
6158 assert!(role.assume_role_policy_document.contains("Statement"));
6159 let attached = acct.role_policies.get("r1").unwrap();
6161 assert_eq!(
6162 attached,
6163 &vec!["arn:aws:iam::aws:policy/AdministratorAccess".to_string()]
6164 );
6165 assert!(acct
6166 .role_inline_policies
6167 .get("r1")
6168 .unwrap()
6169 .contains_key("inline1"));
6170 }
6171
6172 #[test]
6173 fn update_stack_bumps_managed_policy_version() {
6174 let prov = make_provisioner();
6175 let created = prov
6176 .create_resource(&make_resource(
6177 "AWS::IAM::ManagedPolicy",
6178 "P",
6179 serde_json::json!({
6180 "ManagedPolicyName": "p1",
6181 "PolicyDocument": {"Version": "2012-10-17", "Statement": [{"Effect": "Allow"}]},
6182 }),
6183 ))
6184 .expect("managed policy provisions");
6185 prov.update_resource(
6186 &created,
6187 &make_resource(
6188 "AWS::IAM::ManagedPolicy",
6189 "P",
6190 serde_json::json!({
6191 "ManagedPolicyName": "p1",
6192 "PolicyDocument": {"Version": "2012-10-17", "Statement": [{"Effect": "Deny"}]},
6193 }),
6194 ),
6195 )
6196 .expect("update succeeds")
6197 .expect("IAM::ManagedPolicy is updatable");
6198 let iam = prov.iam_state.read();
6199 let acct = iam.get("123456789012").unwrap();
6200 let policy = acct.policies.get(&created.physical_id).unwrap();
6201 assert_eq!(policy.versions.len(), 2);
6202 assert_eq!(policy.default_version_id, "v2");
6203 let default = policy.versions.iter().find(|v| v.is_default).unwrap();
6204 assert!(default.document.contains("Deny"));
6205 }
6206
6207 #[test]
6208 fn update_stack_applies_sqs_queue_property_change() {
6209 let prov = make_provisioner();
6210 let created = prov
6211 .create_resource(&make_resource(
6212 "AWS::SQS::Queue",
6213 "Q",
6214 serde_json::json!({ "QueueName": "q1", "VisibilityTimeout": "30" }),
6215 ))
6216 .expect("queue provisions");
6217 let updated = prov
6219 .update_resource(
6220 &created,
6221 &make_resource(
6222 "AWS::SQS::Queue",
6223 "Q",
6224 serde_json::json!({ "QueueName": "q1", "VisibilityTimeout": "120" }),
6225 ),
6226 )
6227 .expect("update succeeds")
6228 .expect("SQS::Queue is an updatable type");
6229 assert_eq!(updated.physical_id, created.physical_id);
6230 let sqs = prov.sqs_state.read();
6231 let acct = sqs.get("123456789012").unwrap();
6232 let queue = acct.queues.get(&created.physical_id).unwrap();
6233 assert_eq!(
6234 queue
6235 .attributes
6236 .get("VisibilityTimeout")
6237 .map(String::as_str),
6238 Some("120")
6239 );
6240 }
6241
6242 #[test]
6243 fn update_stack_applies_sns_topic_property_change() {
6244 let prov = make_provisioner();
6245 let created = prov
6246 .create_resource(&make_resource(
6247 "AWS::SNS::Topic",
6248 "T",
6249 serde_json::json!({ "TopicName": "t1", "DisplayName": "before" }),
6250 ))
6251 .expect("topic provisions");
6252 let updated = prov
6253 .update_resource(
6254 &created,
6255 &make_resource(
6256 "AWS::SNS::Topic",
6257 "T",
6258 serde_json::json!({ "TopicName": "t1", "DisplayName": "after" }),
6259 ),
6260 )
6261 .expect("update succeeds")
6262 .expect("SNS::Topic is an updatable type");
6263 assert_eq!(updated.physical_id, created.physical_id);
6264 let sns = prov.sns_state.read();
6265 let acct = sns.get("123456789012").unwrap();
6266 let topic = acct.topics.get(&created.physical_id).unwrap();
6267 assert_eq!(
6268 topic.attributes.get("DisplayName").map(String::as_str),
6269 Some("after")
6270 );
6271 }
6272
6273 #[test]
6274 fn ec2_vpc_subnet_provision_through_real_handlers() {
6275 let prov = make_provisioner();
6276 let vpc = prov
6277 .create_resource(&make_resource(
6278 "AWS::EC2::VPC",
6279 "Vpc",
6280 serde_json::json!({ "CidrBlock": "10.1.0.0/16" }),
6281 ))
6282 .expect("VPC provisions");
6283 assert!(
6284 vpc.physical_id.starts_with("vpc-"),
6285 "got {}",
6286 vpc.physical_id
6287 );
6288 {
6291 let ec2 = prov.ec2_state.read();
6292 let acct = ec2.get("123456789012").unwrap();
6293 assert!(acct.vpcs.contains_key(&vpc.physical_id));
6294 }
6295 assert_eq!(
6297 prov.get_att(&vpc, "VpcId").as_deref(),
6298 Some(vpc.physical_id.as_str())
6299 );
6300 assert_eq!(
6301 prov.get_att(&vpc, "CidrBlock").as_deref(),
6302 Some("10.1.0.0/16")
6303 );
6304
6305 let subnet = prov
6307 .create_resource(&make_resource(
6308 "AWS::EC2::Subnet",
6309 "Subnet",
6310 serde_json::json!({ "VpcId": vpc.physical_id, "CidrBlock": "10.1.1.0/24" }),
6311 ))
6312 .expect("subnet provisions");
6313 assert!(subnet.physical_id.starts_with("subnet-"));
6314 {
6315 let ec2 = prov.ec2_state.read();
6316 let acct = ec2.get("123456789012").unwrap();
6317 assert!(acct.subnets.contains_key(&subnet.physical_id));
6318 }
6319
6320 prov.delete_resource(&subnet).expect("subnet deletes");
6322 prov.delete_resource(&vpc).expect("vpc deletes");
6323 }
6324
6325 #[test]
6326 fn unknown_resource_type_records_instead_of_failing() {
6327 let prov = make_provisioner();
6328 let sr = prov
6332 .create_resource(&make_resource(
6333 "AWS::CloudFormation::WaitConditionHandle",
6334 "Handle",
6335 serde_json::json!({}),
6336 ))
6337 .expect("unknown resource type should record, not fail");
6338 assert_eq!(sr.physical_id, "Handle");
6339 assert_eq!(sr.status, "CREATE_COMPLETE");
6340 prov.delete_resource(&sr)
6342 .expect("delete no-op should succeed");
6343 }
6344
6345 #[test]
6346 fn ecr_repository_uri_uses_bound_endpoint_not_public_dns() {
6347 let prov = make_provisioner();
6351 let sr = prov
6352 .create_resource(&make_resource(
6353 "AWS::ECR::Repository",
6354 "Repo",
6355 serde_json::json!({ "RepositoryName": "my-repo" }),
6356 ))
6357 .expect("ECR repo provisions");
6358 let uri = sr
6359 .attributes
6360 .get("RepositoryUri")
6361 .expect("RepositoryUri attribute");
6362 assert!(
6363 !uri.contains("amazonaws.com"),
6364 "RepositoryUri must not use the public ECR DNS, got {uri}"
6365 );
6366 assert!(uri.contains("my-repo"), "uri should name the repo: {uri}");
6367 }
6368
6369 #[test]
6370 fn sns_subscription_rejects_nonexistent_topic() {
6371 let prov = make_provisioner();
6372 let resource = make_resource(
6373 "AWS::SNS::Subscription",
6374 "MySub",
6375 serde_json::json!({
6376 "TopicArn": "arn:aws:sns:us-east-1:123456789012:NonExistent",
6377 "Protocol": "sqs",
6378 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:my-queue"
6379 }),
6380 );
6381 let result = prov.create_resource(&resource);
6382 assert!(result.is_err());
6383 assert!(result.unwrap_err().contains("does not exist"));
6384 }
6385
6386 #[test]
6387 fn sns_subscription_succeeds_when_topic_exists() {
6388 let prov = make_provisioner();
6389 let topic = make_resource(
6391 "AWS::SNS::Topic",
6392 "MyTopic",
6393 serde_json::json!({ "TopicName": "my-topic" }),
6394 );
6395 let topic_result = prov.create_resource(&topic);
6396 assert!(topic_result.is_ok());
6397 let topic_arn = topic_result.unwrap().physical_id;
6398
6399 let sub = make_resource(
6401 "AWS::SNS::Subscription",
6402 "MySub",
6403 serde_json::json!({
6404 "TopicArn": topic_arn,
6405 "Protocol": "sqs",
6406 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:my-queue"
6407 }),
6408 );
6409 let result = prov.create_resource(&sub);
6410 assert!(result.is_ok());
6411 }
6412
6413 #[test]
6414 fn eventbridge_rule_arn_default_bus_omits_bus_name() {
6415 let prov = make_provisioner();
6416 let resource = make_resource(
6417 "AWS::Events::Rule",
6418 "MyRule",
6419 serde_json::json!({
6420 "Name": "my-rule",
6421 "ScheduleExpression": "rate(1 hour)"
6422 }),
6423 );
6424 let result = prov.create_resource(&resource).unwrap();
6425 assert_eq!(
6427 result.physical_id,
6428 "arn:aws:events:us-east-1:123456789012:rule/my-rule"
6429 );
6430 assert!(!result.physical_id.contains("rule/default/"));
6431 }
6432
6433 #[test]
6434 fn eventbridge_rule_arn_custom_bus_includes_bus_name() {
6435 let prov = make_provisioner();
6436 {
6438 let mut eb_accounts = prov.eventbridge_state.write();
6439 let state = eb_accounts.default_mut();
6440 state.buses.insert(
6441 "custom-bus".to_string(),
6442 fakecloud_eventbridge::EventBus {
6443 name: "custom-bus".to_string(),
6444 arn: "arn:aws:events:us-east-1:123456789012:event-bus/custom-bus".to_string(),
6445 policy: None,
6446 creation_time: Utc::now(),
6447 last_modified_time: Utc::now(),
6448 description: None,
6449 kms_key_identifier: None,
6450 dead_letter_config: None,
6451 tags: std::collections::BTreeMap::new(),
6452 },
6453 );
6454 }
6455 let resource = make_resource(
6456 "AWS::Events::Rule",
6457 "MyRule",
6458 serde_json::json!({
6459 "Name": "my-rule",
6460 "EventBusName": "custom-bus",
6461 "ScheduleExpression": "rate(1 hour)"
6462 }),
6463 );
6464 let result = prov.create_resource(&resource).unwrap();
6465 assert_eq!(
6466 result.physical_id,
6467 "arn:aws:events:us-east-1:123456789012:rule/custom-bus/my-rule"
6468 );
6469 }
6470
6471 #[test]
6472 fn eventbridge_rule_rejects_nonexistent_bus() {
6473 let prov = make_provisioner();
6474 let resource = make_resource(
6475 "AWS::Events::Rule",
6476 "MyRule",
6477 serde_json::json!({
6478 "Name": "my-rule",
6479 "EventBusName": "nonexistent-bus",
6480 "ScheduleExpression": "rate(1 hour)"
6481 }),
6482 );
6483 let result = prov.create_resource(&resource);
6484 assert!(result.is_err());
6485 assert!(result.unwrap_err().contains("does not exist"));
6486 }
6487
6488 #[test]
6489 fn custom_resource_requires_service_token() {
6490 let prov = make_provisioner();
6491 let resource = make_resource(
6492 "Custom::MyResource",
6493 "MyCustom",
6494 serde_json::json!({
6495 "Foo": "bar"
6496 }),
6497 );
6498 let result = prov.create_resource(&resource);
6499 assert!(result.is_err());
6500 assert!(
6501 result.unwrap_err().contains("ServiceToken"),
6502 "Should require ServiceToken property"
6503 );
6504 }
6505
6506 #[test]
6507 fn custom_resource_succeeds_without_lambda_delivery() {
6508 let prov = make_provisioner();
6511 let resource = make_resource(
6512 "Custom::MyResource",
6513 "MyCustom",
6514 serde_json::json!({
6515 "ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:my-func",
6516 "Foo": "bar"
6517 }),
6518 );
6519 let result = prov.create_resource(&resource);
6520 assert!(result.is_ok());
6521 let sr = result.unwrap();
6522 assert_eq!(sr.logical_id, "MyCustom");
6523 assert_eq!(sr.resource_type, "Custom::MyResource");
6524 assert!(sr.physical_id.starts_with("MyCustom-"));
6525 }
6526
6527 #[test]
6528 fn cloudformation_custom_resource_type_succeeds() {
6529 let prov = make_provisioner();
6530 let resource = make_resource(
6531 "AWS::CloudFormation::CustomResource",
6532 "MyCustom2",
6533 serde_json::json!({
6534 "ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:my-func",
6535 "Key": "value"
6536 }),
6537 );
6538 let result = prov.create_resource(&resource);
6539 assert!(result.is_ok());
6540 let sr = result.unwrap();
6541 assert_eq!(sr.resource_type, "AWS::CloudFormation::CustomResource");
6542 }
6543
6544 #[test]
6547 fn sqs_queue_create_and_delete() {
6548 let prov = make_provisioner();
6549 let res = make_resource(
6550 "AWS::SQS::Queue",
6551 "MyQ",
6552 serde_json::json!({"QueueName": "my-q"}),
6553 );
6554 let sr = prov.create_resource(&res).unwrap();
6555 assert!(sr.physical_id.contains("my-q"));
6556 assert_eq!(sr.resource_type, "AWS::SQS::Queue");
6557 prov.delete_resource(&sr).unwrap();
6558 }
6559
6560 #[test]
6561 fn sqs_queue_fifo_with_suffix() {
6562 let prov = make_provisioner();
6563 let res = make_resource(
6564 "AWS::SQS::Queue",
6565 "FifoQ",
6566 serde_json::json!({"QueueName": "my-fifo.fifo", "FifoQueue": true}),
6567 );
6568 let sr = prov.create_resource(&res).unwrap();
6569 assert!(sr.physical_id.contains(".fifo"));
6570 }
6571
6572 #[test]
6573 fn sns_topic_create_and_delete() {
6574 let prov = make_provisioner();
6575 let res = make_resource(
6576 "AWS::SNS::Topic",
6577 "MyTopic",
6578 serde_json::json!({"TopicName": "t1"}),
6579 );
6580 let sr = prov.create_resource(&res).unwrap();
6581 assert!(sr.physical_id.contains("t1"));
6582 prov.delete_resource(&sr).unwrap();
6583 }
6584
6585 #[test]
6586 fn ssm_parameter_create_and_delete() {
6587 let prov = make_provisioner();
6588 let res = make_resource(
6589 "AWS::SSM::Parameter",
6590 "MyParam",
6591 serde_json::json!({
6592 "Name": "/my/param",
6593 "Type": "String",
6594 "Value": "v1"
6595 }),
6596 );
6597 let sr = prov.create_resource(&res).unwrap();
6598 assert_eq!(sr.physical_id, "/my/param");
6599 prov.delete_resource(&sr).unwrap();
6600 }
6601
6602 #[test]
6603 fn iam_role_create_and_delete() {
6604 let prov = make_provisioner();
6605 let res = make_resource(
6606 "AWS::IAM::Role",
6607 "MyRole",
6608 serde_json::json!({
6609 "RoleName": "my-role",
6610 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
6611 }),
6612 );
6613 let sr = prov.create_resource(&res).unwrap();
6614 assert!(sr.physical_id.contains("my-role"));
6615 prov.delete_resource(&sr).unwrap();
6616 }
6617
6618 #[test]
6619 fn iam_policy_create_and_delete() {
6620 let prov = make_provisioner();
6621 let res = make_resource(
6622 "AWS::IAM::Policy",
6623 "MyPolicy",
6624 serde_json::json!({
6625 "PolicyName": "my-policy",
6626 "PolicyDocument": {"Version": "2012-10-17", "Statement": []}
6627 }),
6628 );
6629 let sr = prov.create_resource(&res).unwrap();
6630 assert!(sr.physical_id.contains("my-policy"));
6631 prov.delete_resource(&sr).unwrap();
6632 }
6633
6634 #[test]
6635 fn s3_bucket_create_and_delete() {
6636 let prov = make_provisioner();
6637 let res = make_resource(
6638 "AWS::S3::Bucket",
6639 "MyBucket",
6640 serde_json::json!({"BucketName": "my-bucket"}),
6641 );
6642 let sr = prov.create_resource(&res).unwrap();
6643 assert_eq!(sr.physical_id, "my-bucket");
6644 prov.delete_resource(&sr).unwrap();
6645 }
6646
6647 #[test]
6648 fn sqs_queue_policy_stored_on_queue_and_cleared_on_delete() {
6649 let prov = make_provisioner();
6650 let queue = prov
6651 .create_resource(&make_resource(
6652 "AWS::SQS::Queue",
6653 "Q",
6654 serde_json::json!({"QueueName": "q1"}),
6655 ))
6656 .unwrap();
6657 let policy = make_resource(
6659 "AWS::SQS::QueuePolicy",
6660 "QP",
6661 serde_json::json!({
6662 "Queues": [queue.physical_id.clone()],
6663 "PolicyDocument": {"Version": "2012-10-17", "Statement": [{
6664 "Effect": "Allow",
6665 "Principal": {"Service": "sns.amazonaws.com"},
6666 "Action": "sqs:SendMessage",
6667 "Resource": "*"
6668 }]}
6669 }),
6670 );
6671 let sr = prov.create_resource(&policy).unwrap();
6672
6673 {
6674 let mut accounts = prov.sqs_state.write();
6675 let state = accounts.get_or_create(&prov.account_id);
6676 let stored = state.queues[&queue.physical_id]
6677 .attributes
6678 .get("Policy")
6679 .expect("policy stored on queue");
6680 assert!(stored.contains("sqs:SendMessage"));
6681 }
6682
6683 prov.delete_resource(&sr).unwrap();
6684 {
6685 let mut accounts = prov.sqs_state.write();
6686 let state = accounts.get_or_create(&prov.account_id);
6687 assert!(!state.queues[&queue.physical_id]
6688 .attributes
6689 .contains_key("Policy"));
6690 }
6691 }
6692
6693 #[test]
6694 fn sns_topic_policy_stored_on_topic_and_cleared_on_delete() {
6695 let prov = make_provisioner();
6696 let topic = prov
6697 .create_resource(&make_resource(
6698 "AWS::SNS::Topic",
6699 "T",
6700 serde_json::json!({"TopicName": "t1"}),
6701 ))
6702 .unwrap();
6703 let policy = make_resource(
6704 "AWS::SNS::TopicPolicy",
6705 "TP",
6706 serde_json::json!({
6707 "Topics": [topic.physical_id.clone()],
6708 "PolicyDocument": {"Version": "2012-10-17", "Statement": [{
6709 "Effect": "Allow",
6710 "Principal": {"Service": "events.amazonaws.com"},
6711 "Action": "sns:Publish",
6712 "Resource": "*"
6713 }]}
6714 }),
6715 );
6716 let sr = prov.create_resource(&policy).unwrap();
6717
6718 {
6719 let mut accounts = prov.sns_state.write();
6720 let state = accounts.get_or_create(&prov.account_id);
6721 let stored = state.topics[&topic.physical_id]
6722 .attributes
6723 .get("Policy")
6724 .expect("policy stored on topic");
6725 assert!(stored.contains("sns:Publish"));
6726 }
6727
6728 prov.delete_resource(&sr).unwrap();
6729 {
6730 let mut accounts = prov.sns_state.write();
6731 let state = accounts.get_or_create(&prov.account_id);
6732 assert!(!state.topics[&topic.physical_id]
6733 .attributes
6734 .contains_key("Policy"));
6735 }
6736 }
6737
6738 #[test]
6739 fn s3_bucket_policy_stored_on_bucket_and_cleared_on_delete() {
6740 let prov = make_provisioner();
6741 let bucket = prov
6742 .create_resource(&make_resource(
6743 "AWS::S3::Bucket",
6744 "B",
6745 serde_json::json!({"BucketName": "b1"}),
6746 ))
6747 .unwrap();
6748 let policy = make_resource(
6749 "AWS::S3::BucketPolicy",
6750 "BP",
6751 serde_json::json!({
6752 "Bucket": bucket.physical_id.clone(),
6753 "PolicyDocument": {"Version": "2012-10-17", "Statement": [{
6754 "Effect": "Allow",
6755 "Principal": "*",
6756 "Action": "s3:GetObject",
6757 "Resource": "arn:aws:s3:::b1/*"
6758 }]}
6759 }),
6760 );
6761 let sr = prov.create_resource(&policy).unwrap();
6762 assert_eq!(sr.physical_id, "b1-policy");
6763
6764 {
6765 let mut accounts = prov.s3_state.write();
6766 let state = accounts.get_or_create(&prov.account_id);
6767 let stored = state.buckets[&bucket.physical_id]
6768 .policy
6769 .as_ref()
6770 .expect("policy stored on bucket");
6771 assert!(stored.contains("s3:GetObject"));
6772 }
6773
6774 prov.delete_resource(&sr).unwrap();
6775 {
6776 let mut accounts = prov.s3_state.write();
6777 let state = accounts.get_or_create(&prov.account_id);
6778 assert!(state.buckets[&bucket.physical_id].policy.is_none());
6779 }
6780 }
6781
6782 #[test]
6783 fn dynamodb_table_create_and_delete() {
6784 let prov = make_provisioner();
6785 let res = make_resource(
6786 "AWS::DynamoDB::Table",
6787 "MyTable",
6788 serde_json::json!({
6789 "TableName": "my-table",
6790 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
6791 "AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
6792 "BillingMode": "PAY_PER_REQUEST"
6793 }),
6794 );
6795 let sr = prov.create_resource(&res).unwrap();
6796 assert!(sr.physical_id.contains("my-table"));
6797 prov.delete_resource(&sr).unwrap();
6798 }
6799
6800 #[test]
6801 fn log_group_create_and_delete() {
6802 let prov = make_provisioner();
6803 let res = make_resource(
6804 "AWS::Logs::LogGroup",
6805 "MyLogs",
6806 serde_json::json!({"LogGroupName": "/app/logs"}),
6807 );
6808 let sr = prov.create_resource(&res).unwrap();
6809 assert!(sr.physical_id.contains("/app/logs"));
6810 prov.delete_resource(&sr).unwrap();
6811 }
6812
6813 #[test]
6814 fn lambda_function_create_and_delete() {
6815 let prov = make_provisioner();
6816 let res = make_resource(
6817 "AWS::Lambda::Function",
6818 "MyFn",
6819 serde_json::json!({
6820 "FunctionName": "my-fn",
6821 "Runtime": "nodejs20.x",
6822 "Role": "arn:aws:iam::123456789012:role/lambda-role",
6823 "Handler": "index.handler",
6824 "MemorySize": 256,
6825 "Timeout": 10,
6826 "Environment": {"Variables": {"FOO": "bar"}}
6827 }),
6828 );
6829 let sr = prov.create_resource(&res).unwrap();
6830 assert_eq!(sr.physical_id, "my-fn");
6831 assert_eq!(
6832 sr.attributes.get("Arn").map(String::as_str),
6833 Some("arn:aws:lambda:us-east-1:123456789012:function:my-fn")
6834 );
6835 {
6837 let lam = prov.lambda_state.read();
6838 let st = lam.get("123456789012").unwrap();
6839 let f = st.functions.get("my-fn").unwrap();
6840 assert_eq!(f.runtime, "nodejs20.x");
6841 assert_eq!(f.memory_size, 256);
6842 assert_eq!(f.environment.get("FOO").unwrap(), "bar");
6843 }
6844 prov.delete_resource(&sr).unwrap();
6845 let lam = prov.lambda_state.read();
6846 let st = lam.get("123456789012").unwrap();
6847 assert!(!st.functions.contains_key("my-fn"));
6848 }
6849
6850 #[test]
6851 fn unsupported_resource_type_is_recorded_not_failed() {
6852 let prov = make_provisioner();
6856 let res = make_resource("AWS::NonExistent::Thing", "X", serde_json::json!({}));
6857 let sr = prov.create_resource(&res).unwrap();
6858 assert_eq!(sr.physical_id, "X");
6859 }
6860
6861 #[test]
6862 fn iam_role_with_inline_policies() {
6863 let prov = make_provisioner();
6864 let res = make_resource(
6865 "AWS::IAM::Role",
6866 "MyRole",
6867 serde_json::json!({
6868 "RoleName": "role-inline",
6869 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []},
6870 "Policies": [
6871 {
6872 "PolicyName": "inline-1",
6873 "PolicyDocument": {"Version": "2012-10-17", "Statement": []}
6874 }
6875 ]
6876 }),
6877 );
6878 let sr = prov.create_resource(&res).unwrap();
6879 assert!(sr.physical_id.contains("role-inline"));
6880 }
6881
6882 #[test]
6883 fn sqs_queue_auto_name() {
6884 let prov = make_provisioner();
6885 let res = make_resource("AWS::SQS::Queue", "AutoQ", serde_json::json!({}));
6886 let sr = prov.create_resource(&res).unwrap();
6887 assert!(!sr.physical_id.is_empty());
6889 }
6890
6891 #[test]
6892 fn sns_topic_auto_name() {
6893 let prov = make_provisioner();
6894 let res = make_resource("AWS::SNS::Topic", "AutoT", serde_json::json!({}));
6895 let sr = prov.create_resource(&res).unwrap();
6896 assert!(!sr.physical_id.is_empty());
6897 }
6898
6899 #[test]
6902 fn unsupported_resource_type_recorded_with_logical_id() {
6903 let prov = make_provisioner();
6905 let res = make_resource("AWS::FooBar::Thing", "X", serde_json::json!({}));
6906 let sr = prov.create_resource(&res).unwrap();
6907 assert_eq!(sr.physical_id, "X");
6908 assert_eq!(sr.status, "CREATE_COMPLETE");
6909 }
6910
6911 #[test]
6912 fn sqs_queue_with_redrive_policy() {
6913 let prov = make_provisioner();
6914 let dlq = make_resource(
6916 "AWS::SQS::Queue",
6917 "DLQ",
6918 serde_json::json!({"QueueName": "dlq1"}),
6919 );
6920 let dlq_resource = prov.create_resource(&dlq).unwrap();
6921 let _ = dlq_resource.physical_id;
6922
6923 let src = make_resource(
6925 "AWS::SQS::Queue",
6926 "Src",
6927 serde_json::json!({
6928 "QueueName": "src1",
6929 "RedrivePolicy": {
6930 "deadLetterTargetArn": "arn:aws:sqs:us-east-1:123456789012:dlq1",
6931 "maxReceiveCount": 3
6932 }
6933 }),
6934 );
6935 let sr = prov.create_resource(&src).unwrap();
6936 assert!(!sr.physical_id.is_empty());
6937 }
6938
6939 #[test]
6940 fn sns_topic_with_display_name() {
6941 let prov = make_provisioner();
6942 let res = make_resource(
6943 "AWS::SNS::Topic",
6944 "WithName",
6945 serde_json::json!({"TopicName": "named-topic", "DisplayName": "Named"}),
6946 );
6947 let sr = prov.create_resource(&res).unwrap();
6948 assert!(sr.physical_id.contains("named-topic"));
6949 }
6950
6951 #[test]
6952 fn ssm_parameter_with_explicit_name() {
6953 let prov = make_provisioner();
6954 let res = make_resource(
6955 "AWS::SSM::Parameter",
6956 "Param",
6957 serde_json::json!({"Name": "/my/param", "Value": "v", "Type": "String"}),
6958 );
6959 let sr = prov.create_resource(&res).unwrap();
6960 assert!(sr.physical_id.contains("/my/param"));
6961 }
6962
6963 #[test]
6964 fn ssm_parameter_missing_name_errors() {
6965 let prov = make_provisioner();
6966 let res = make_resource(
6967 "AWS::SSM::Parameter",
6968 "AutoP",
6969 serde_json::json!({"Value": "v", "Type": "String"}),
6970 );
6971 assert!(prov.create_resource(&res).is_err());
6972 }
6973
6974 #[test]
6975 fn iam_managed_policy_auto_name() {
6976 let prov = make_provisioner();
6977 let res = make_resource(
6978 "AWS::IAM::Policy",
6979 "AutoPol",
6980 serde_json::json!({
6981 "PolicyName": "inline-pol",
6982 "PolicyDocument": {"Version": "2012-10-17", "Statement": []},
6983 "Users": []
6984 }),
6985 );
6986 let sr = prov.create_resource(&res).unwrap();
6987 assert!(!sr.physical_id.is_empty());
6988 }
6989
6990 #[test]
6991 fn delete_resource_works_for_queue() {
6992 let prov = make_provisioner();
6993 let res = make_resource(
6994 "AWS::SQS::Queue",
6995 "ToDel",
6996 serde_json::json!({"QueueName": "todel"}),
6997 );
6998 let sr = prov.create_resource(&res).unwrap();
6999 assert!(prov.delete_resource(&sr).is_ok());
7000 }
7001
7002 #[test]
7003 fn delete_resource_works_for_topic() {
7004 let prov = make_provisioner();
7005 let res = make_resource(
7006 "AWS::SNS::Topic",
7007 "DelT",
7008 serde_json::json!({"TopicName": "delt"}),
7009 );
7010 let sr = prov.create_resource(&res).unwrap();
7011 assert!(prov.delete_resource(&sr).is_ok());
7012 }
7013
7014 #[test]
7015 fn application_autoscaling_scalable_target_round_trip() {
7016 let prov = make_provisioner();
7017 let res = make_resource(
7018 "AWS::ApplicationAutoScaling::ScalableTarget",
7019 "Target",
7020 serde_json::json!({
7021 "ServiceNamespace": "ecs",
7022 "ResourceId": "service/my-cluster/my-service",
7023 "ScalableDimension": "ecs:service:DesiredCount",
7024 "MinCapacity": 1,
7025 "MaxCapacity": 10,
7026 "RoleARN": "arn:aws:iam::123456789012:role/my-role",
7027 }),
7028 );
7029 let sr = prov.create_resource(&res).unwrap();
7030 assert_eq!(sr.physical_id, "service/my-cluster/my-service");
7031 assert!(sr.attributes.contains_key("ScalableTargetARN"));
7032 assert!(prov.delete_resource(&sr).is_ok());
7033 }
7034
7035 #[test]
7036 fn application_autoscaling_scaling_policy_requires_target() {
7037 let prov = make_provisioner();
7038 let res = make_resource(
7039 "AWS::ApplicationAutoScaling::ScalingPolicy",
7040 "Policy",
7041 serde_json::json!({
7042 "PolicyName": "my-policy",
7043 "ServiceNamespace": "ecs",
7044 "ResourceId": "service/my-cluster/my-service",
7045 "ScalableDimension": "ecs:service:DesiredCount",
7046 "PolicyType": "TargetTrackingScaling",
7047 "TargetTrackingScalingPolicyConfiguration": {
7048 "TargetValue": 50.0,
7049 "PredefinedMetricSpecification": {
7050 "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
7051 }
7052 },
7053 }),
7054 );
7055 assert!(prov.create_resource(&res).is_err());
7057 }
7058
7059 #[test]
7060 fn application_autoscaling_scaling_policy_round_trip() {
7061 let prov = make_provisioner();
7062 let target = make_resource(
7063 "AWS::ApplicationAutoScaling::ScalableTarget",
7064 "Target",
7065 serde_json::json!({
7066 "ServiceNamespace": "ecs",
7067 "ResourceId": "service/my-cluster/my-service",
7068 "ScalableDimension": "ecs:service:DesiredCount",
7069 "MinCapacity": 1,
7070 "MaxCapacity": 10,
7071 }),
7072 );
7073 let sr = prov.create_resource(&target).unwrap();
7074
7075 let policy = make_resource(
7076 "AWS::ApplicationAutoScaling::ScalingPolicy",
7077 "Policy",
7078 serde_json::json!({
7079 "PolicyName": "my-policy",
7080 "ServiceNamespace": "ecs",
7081 "ResourceId": "service/my-cluster/my-service",
7082 "ScalableDimension": "ecs:service:DesiredCount",
7083 "PolicyType": "TargetTrackingScaling",
7084 "TargetTrackingScalingPolicyConfiguration": {
7085 "TargetValue": 50.0,
7086 "PredefinedMetricSpecification": {
7087 "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
7088 }
7089 },
7090 }),
7091 );
7092 let psr = prov.create_resource(&policy).unwrap();
7093 assert!(psr.physical_id.starts_with("arn:aws:autoscaling:"));
7094 assert!(prov.delete_resource(&psr).is_ok());
7095 assert!(prov.delete_resource(&sr).is_ok());
7096 }
7097
7098 #[test]
7099 fn sqs_queue_with_fifo_suffix() {
7100 let prov = make_provisioner();
7101 let res = make_resource(
7102 "AWS::SQS::Queue",
7103 "Fifo",
7104 serde_json::json!({"QueueName": "fq.fifo", "FifoQueue": true}),
7105 );
7106 let sr = prov.create_resource(&res).unwrap();
7107 assert!(sr.physical_id.ends_with(".fifo"));
7108 }
7109
7110 #[test]
7113 fn getatt_s3_bucket_arn_returns_arn() {
7114 let prov = make_provisioner();
7115 let bucket = make_resource(
7116 "AWS::S3::Bucket",
7117 "MyBucket",
7118 serde_json::json!({"BucketName": "my-bucket"}),
7119 );
7120 let sr = prov.create_resource(&bucket).unwrap();
7121 assert_eq!(
7122 prov.get_att(&sr, "Arn"),
7123 Some("arn:aws:s3:::my-bucket".to_string())
7124 );
7125 }
7126
7127 #[test]
7128 fn getatt_s3_bucket_domain_name_returns_dns_name() {
7129 let prov = make_provisioner();
7130 let bucket = make_resource(
7131 "AWS::S3::Bucket",
7132 "MyBucket",
7133 serde_json::json!({"BucketName": "my-bucket"}),
7134 );
7135 let sr = prov.create_resource(&bucket).unwrap();
7136 assert_eq!(
7137 prov.get_att(&sr, "DomainName"),
7138 Some("my-bucket.s3.amazonaws.com".to_string())
7139 );
7140 }
7141
7142 #[test]
7143 fn getatt_lambda_function_arn_returns_function_arn() {
7144 let prov = make_provisioner();
7145 let role = make_resource(
7147 "AWS::IAM::Role",
7148 "MyRole",
7149 serde_json::json!({
7150 "RoleName": "my-role",
7151 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
7152 }),
7153 );
7154 let role_sr = prov.create_resource(&role).unwrap();
7155 let fn_res = make_resource(
7156 "AWS::Lambda::Function",
7157 "MyFn",
7158 serde_json::json!({
7159 "FunctionName": "my-fn",
7160 "Runtime": "python3.11",
7161 "Handler": "index.handler",
7162 "Role": role_sr.physical_id,
7163 "Code": {"ZipFile": "def handler(e,c): return e"}
7164 }),
7165 );
7166 let fn_sr = prov.create_resource(&fn_res).unwrap();
7167 let arn = prov.get_att(&fn_sr, "Arn").expect("Arn should resolve");
7168 assert!(arn.starts_with("arn:aws:lambda:"));
7169 assert!(arn.contains(":function:my-fn"));
7170 }
7171
7172 #[test]
7173 fn getatt_iam_role_arn_returns_role_arn() {
7174 let prov = make_provisioner();
7175 let role = make_resource(
7176 "AWS::IAM::Role",
7177 "MyRole",
7178 serde_json::json!({
7179 "RoleName": "my-role",
7180 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
7181 }),
7182 );
7183 let sr = prov.create_resource(&role).unwrap();
7184 assert_eq!(
7185 prov.get_att(&sr, "Arn"),
7186 Some("arn:aws:iam::123456789012:role/my-role".to_string())
7187 );
7188 let role_id = prov.get_att(&sr, "RoleId").expect("RoleId should resolve");
7190 assert!(role_id.starts_with("FKIA"));
7191 }
7192
7193 #[test]
7194 fn getatt_unknown_attribute_returns_none() {
7195 let prov = make_provisioner();
7196 let bucket = make_resource(
7197 "AWS::S3::Bucket",
7198 "MyBucket",
7199 serde_json::json!({"BucketName": "my-bucket"}),
7200 );
7201 let sr = prov.create_resource(&bucket).unwrap();
7202 assert_eq!(prov.get_att(&sr, "NotARealAttr"), None);
7206 }
7207
7208 #[test]
7209 fn getatt_unknown_resource_type_returns_none() {
7210 let prov = make_provisioner();
7211 let stack_resource = StackResource {
7215 logical_id: "Mystery".to_string(),
7216 physical_id: "mystery-id".to_string(),
7217 resource_type: "AWS::Made::Up".to_string(),
7218 status: "CREATE_COMPLETE".to_string(),
7219 service_token: None,
7220 attributes: BTreeMap::new(),
7221 };
7222 assert_eq!(prov.get_att(&stack_resource, "Arn"), None);
7223 }
7224
7225 #[test]
7226 fn getatt_falls_back_to_captured_attributes() {
7227 let prov = make_provisioner();
7228 let stack_resource = StackResource {
7232 logical_id: "MyTopic".to_string(),
7233 physical_id: "arn:aws:sns:us-east-1:123456789012:my-topic".to_string(),
7234 resource_type: "AWS::SNS::Topic".to_string(),
7235 status: "CREATE_COMPLETE".to_string(),
7236 service_token: None,
7237 attributes: {
7238 let mut m = BTreeMap::new();
7239 m.insert("TopicArn".to_string(), "captured-arn".to_string());
7240 m
7241 },
7242 };
7243 assert_eq!(
7244 prov.get_att(&stack_resource, "TopicArn"),
7245 Some("captured-arn".to_string())
7246 );
7247 }
7248
7249 #[test]
7250 fn getatt_secrets_manager_arn_resolves_via_live_state() {
7251 let prov = make_provisioner();
7254 let res = make_resource(
7255 "AWS::SecretsManager::Secret",
7256 "MySecret",
7257 serde_json::json!({"Name": "my-secret", "SecretString": "hunter2"}),
7258 );
7259 let sr = prov.create_resource(&res).unwrap();
7260 let arn = prov.get_att(&sr, "Arn").expect("Arn should resolve");
7261 assert!(arn.starts_with("arn:aws:secretsmanager:"));
7262 assert!(arn.ends_with(":secret:my-secret"));
7263 }
7264
7265 #[test]
7266 fn wafv2_web_acl_lifecycle() {
7267 let prov = make_provisioner();
7268 let res = make_resource(
7269 "AWS::WAFv2::WebACL",
7270 "MyAcl",
7271 serde_json::json!({
7272 "Name": "my-acl",
7273 "Scope": "REGIONAL",
7274 "DefaultAction": {"Allow": {}},
7275 "Rules": [{"Name": "rule1", "Priority": 1, "Statement": {}, "VisibilityConfig": {}}],
7276 "VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "my-acl-metric"},
7277 "Capacity": 100,
7278 }),
7279 );
7280 let sr = prov.create_resource(&res).unwrap();
7281 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
7282 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
7283 assert_eq!(prov.get_att(&sr, "Name"), Some("my-acl".to_string()));
7284 assert!(prov.get_att(&sr, "Id").is_some());
7285 assert_eq!(prov.get_att(&sr, "Capacity"), Some("100".to_string()));
7286
7287 prov.delete_resource(&sr.clone()).unwrap();
7288 let fresh = StackResource {
7291 logical_id: "MyAcl".to_string(),
7292 physical_id: sr.physical_id.clone(),
7293 resource_type: "AWS::WAFv2::WebACL".to_string(),
7294 status: "CREATE_COMPLETE".to_string(),
7295 service_token: None,
7296 attributes: BTreeMap::new(),
7297 };
7298 assert_eq!(prov.get_att(&fresh, "Arn"), None);
7299 }
7300
7301 #[test]
7302 fn wafv2_ip_set_lifecycle() {
7303 let prov = make_provisioner();
7304 let res = make_resource(
7305 "AWS::WAFv2::IPSet",
7306 "MyIpSet",
7307 serde_json::json!({
7308 "Name": "my-ipset",
7309 "Scope": "REGIONAL",
7310 "IPAddressVersion": "IPV4",
7311 "Addresses": ["10.0.0.0/8"],
7312 }),
7313 );
7314 let sr = prov.create_resource(&res).unwrap();
7315 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
7316 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
7317 assert_eq!(prov.get_att(&sr, "Name"), Some("my-ipset".to_string()));
7318
7319 prov.delete_resource(&sr.clone()).unwrap();
7320 let fresh = StackResource {
7321 logical_id: "MyIpSet".to_string(),
7322 physical_id: sr.physical_id.clone(),
7323 resource_type: "AWS::WAFv2::IPSet".to_string(),
7324 status: "CREATE_COMPLETE".to_string(),
7325 service_token: None,
7326 attributes: BTreeMap::new(),
7327 };
7328 assert_eq!(prov.get_att(&fresh, "Arn"), None);
7329 }
7330
7331 #[test]
7332 fn wafv2_regex_pattern_set_lifecycle() {
7333 let prov = make_provisioner();
7334 let res = make_resource(
7335 "AWS::WAFv2::RegexPatternSet",
7336 "MyRegexSet",
7337 serde_json::json!({
7338 "Name": "my-regex",
7339 "Scope": "REGIONAL",
7340 "RegularExpressions": [{"RegexString": "^test"}],
7341 }),
7342 );
7343 let sr = prov.create_resource(&res).unwrap();
7344 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
7345 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
7346 assert_eq!(prov.get_att(&sr, "Name"), Some("my-regex".to_string()));
7347
7348 prov.delete_resource(&sr.clone()).unwrap();
7349 let fresh = StackResource {
7350 logical_id: "MyRegexSet".to_string(),
7351 physical_id: sr.physical_id.clone(),
7352 resource_type: "AWS::WAFv2::RegexPatternSet".to_string(),
7353 status: "CREATE_COMPLETE".to_string(),
7354 service_token: None,
7355 attributes: BTreeMap::new(),
7356 };
7357 assert_eq!(prov.get_att(&fresh, "Arn"), None);
7358 }
7359
7360 #[test]
7361 fn wafv2_rule_group_lifecycle() {
7362 let prov = make_provisioner();
7363 let res = make_resource(
7364 "AWS::WAFv2::RuleGroup",
7365 "MyRuleGroup",
7366 serde_json::json!({
7367 "Name": "my-rg",
7368 "Scope": "REGIONAL",
7369 "Capacity": 50,
7370 "Rules": [{"Name": "r1", "Priority": 1, "Statement": {}, "VisibilityConfig": {}}],
7371 "VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "rg-metric"},
7372 }),
7373 );
7374 let sr = prov.create_resource(&res).unwrap();
7375 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
7376 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
7377 assert_eq!(prov.get_att(&sr, "Name"), Some("my-rg".to_string()));
7378
7379 prov.delete_resource(&sr.clone()).unwrap();
7380 let fresh = StackResource {
7381 logical_id: "MyRuleGroup".to_string(),
7382 physical_id: sr.physical_id.clone(),
7383 resource_type: "AWS::WAFv2::RuleGroup".to_string(),
7384 status: "CREATE_COMPLETE".to_string(),
7385 service_token: None,
7386 attributes: BTreeMap::new(),
7387 };
7388 assert_eq!(prov.get_att(&fresh, "Arn"), None);
7389 }
7390
7391 #[test]
7392 fn wafv2_logging_configuration_lifecycle() {
7393 let prov = make_provisioner();
7394 let res = make_resource(
7395 "AWS::WAFv2::LoggingConfiguration",
7396 "MyLogConfig",
7397 serde_json::json!({
7398 "ResourceArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/abc",
7399 "LogDestinationConfigs": ["arn:aws:logs:us-east-1:123456789012:log-group:/aws/waf"],
7400 }),
7401 );
7402 let sr = prov.create_resource(&res).unwrap();
7403 assert_eq!(
7404 sr.physical_id,
7405 "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/abc"
7406 );
7407
7408 prov.delete_resource(&sr.clone()).unwrap();
7409 }
7410
7411 #[test]
7412 fn wafv2_web_acl_association_lifecycle() {
7413 let prov = make_provisioner();
7414 let res = make_resource(
7415 "AWS::WAFv2::WebACLAssociation",
7416 "MyAssoc",
7417 serde_json::json!({
7418 "ResourceArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188",
7419 "WebACLArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/my-acl/abc",
7420 }),
7421 );
7422 let sr = prov.create_resource(&res).unwrap();
7423 assert_eq!(sr.physical_id, "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188");
7424
7425 prov.delete_resource(&sr.clone()).unwrap();
7426 }
7427
7428 #[test]
7429 fn ses_configuration_set_lifecycle() {
7430 let prov = make_provisioner();
7431 let res = make_resource(
7432 "AWS::SES::ConfigurationSet",
7433 "MyConfigSet",
7434 serde_json::json!({
7435 "Name": "my-cs",
7436 "SendingOptions": {"SendingEnabled": true},
7437 "DeliveryOptions": {"TlsPolicy": "REQUIRE"},
7438 }),
7439 );
7440 let sr = prov.create_resource(&res).unwrap();
7441 assert_eq!(sr.physical_id, "my-cs");
7442 assert_eq!(prov.get_att(&sr, "Name"), Some("my-cs".to_string()));
7443
7444 prov.delete_resource(&sr.clone()).unwrap();
7445 let fresh = StackResource {
7446 logical_id: "MyConfigSet".to_string(),
7447 physical_id: "my-cs".to_string(),
7448 resource_type: "AWS::SES::ConfigurationSet".to_string(),
7449 status: "CREATE_COMPLETE".to_string(),
7450 service_token: None,
7451 attributes: BTreeMap::new(),
7452 };
7453 assert_eq!(prov.get_att(&fresh, "Name"), None);
7454 }
7455
7456 #[test]
7457 fn ses_email_identity_lifecycle() {
7458 let prov = make_provisioner();
7459 let res = make_resource(
7460 "AWS::SES::EmailIdentity",
7461 "MyIdentity",
7462 serde_json::json!({"EmailIdentity": "example.com"}),
7463 );
7464 let sr = prov.create_resource(&res).unwrap();
7465 assert_eq!(sr.physical_id, "example.com");
7466 assert_eq!(
7467 prov.get_att(&sr, "IdentityName"),
7468 Some("example.com".to_string())
7469 );
7470
7471 prov.delete_resource(&sr.clone()).unwrap();
7472 let fresh = StackResource {
7473 logical_id: "MyIdentity".to_string(),
7474 physical_id: "example.com".to_string(),
7475 resource_type: "AWS::SES::EmailIdentity".to_string(),
7476 status: "CREATE_COMPLETE".to_string(),
7477 service_token: None,
7478 attributes: BTreeMap::new(),
7479 };
7480 assert_eq!(prov.get_att(&fresh, "IdentityName"), None);
7481 }
7482
7483 #[test]
7484 fn ses_template_lifecycle() {
7485 let prov = make_provisioner();
7486 let res = make_resource(
7487 "AWS::SES::Template",
7488 "MyTemplate",
7489 serde_json::json!({
7490 "Template": {
7491 "TemplateName": "my-tpl",
7492 "SubjectPart": "Hello",
7493 "HtmlPart": "<h1>Hi</h1>",
7494 "TextPart": "Hi",
7495 },
7496 }),
7497 );
7498 let sr = prov.create_resource(&res).unwrap();
7499 assert_eq!(sr.physical_id, "my-tpl");
7500 assert_eq!(
7501 prov.get_att(&sr, "TemplateName"),
7502 Some("my-tpl".to_string())
7503 );
7504
7505 prov.delete_resource(&sr.clone()).unwrap();
7506 let fresh = StackResource {
7507 logical_id: "MyTemplate".to_string(),
7508 physical_id: "my-tpl".to_string(),
7509 resource_type: "AWS::SES::Template".to_string(),
7510 status: "CREATE_COMPLETE".to_string(),
7511 service_token: None,
7512 attributes: BTreeMap::new(),
7513 };
7514 assert_eq!(prov.get_att(&fresh, "TemplateName"), None);
7515 }
7516
7517 #[test]
7518 fn ses_contact_list_lifecycle() {
7519 let prov = make_provisioner();
7520 let res = make_resource(
7521 "AWS::SES::ContactList",
7522 "MyContactList",
7523 serde_json::json!({
7524 "ContactListName": "my-cl",
7525 "Description": "Test contacts",
7526 "Topics": [{"TopicName": "news", "DisplayName": "Newsletter", "Description": "Weekly news"}],
7527 }),
7528 );
7529 let sr = prov.create_resource(&res).unwrap();
7530 assert_eq!(sr.physical_id, "my-cl");
7531 assert_eq!(
7532 prov.get_att(&sr, "ContactListName"),
7533 Some("my-cl".to_string())
7534 );
7535
7536 prov.delete_resource(&sr.clone()).unwrap();
7537 let fresh = StackResource {
7538 logical_id: "MyContactList".to_string(),
7539 physical_id: "my-cl".to_string(),
7540 resource_type: "AWS::SES::ContactList".to_string(),
7541 status: "CREATE_COMPLETE".to_string(),
7542 service_token: None,
7543 attributes: BTreeMap::new(),
7544 };
7545 assert_eq!(prov.get_att(&fresh, "ContactListName"), None);
7546 }
7547
7548 #[test]
7549 fn ses_dedicated_ip_pool_lifecycle() {
7550 let prov = make_provisioner();
7551 let res = make_resource(
7552 "AWS::SES::DedicatedIpPool",
7553 "MyPool",
7554 serde_json::json!({"PoolName": "my-pool", "ScalingMode": "STANDARD"}),
7555 );
7556 let sr = prov.create_resource(&res).unwrap();
7557 assert_eq!(sr.physical_id, "my-pool");
7558 assert_eq!(prov.get_att(&sr, "PoolName"), Some("my-pool".to_string()));
7559
7560 prov.delete_resource(&sr.clone()).unwrap();
7561 let fresh = StackResource {
7562 logical_id: "MyPool".to_string(),
7563 physical_id: "my-pool".to_string(),
7564 resource_type: "AWS::SES::DedicatedIpPool".to_string(),
7565 status: "CREATE_COMPLETE".to_string(),
7566 service_token: None,
7567 attributes: BTreeMap::new(),
7568 };
7569 assert_eq!(prov.get_att(&fresh, "PoolName"), None);
7570 }
7571
7572 #[test]
7573 fn ses_receipt_rule_set_lifecycle() {
7574 let prov = make_provisioner();
7575 let res = make_resource(
7576 "AWS::SES::ReceiptRuleSet",
7577 "MyRuleSet",
7578 serde_json::json!({"RuleSetName": "my-rs"}),
7579 );
7580 let sr = prov.create_resource(&res).unwrap();
7581 assert_eq!(sr.physical_id, "my-rs");
7582 assert_eq!(prov.get_att(&sr, "RuleSetName"), Some("my-rs".to_string()));
7583
7584 prov.delete_resource(&sr.clone()).unwrap();
7585 let fresh = StackResource {
7586 logical_id: "MyRuleSet".to_string(),
7587 physical_id: "my-rs".to_string(),
7588 resource_type: "AWS::SES::ReceiptRuleSet".to_string(),
7589 status: "CREATE_COMPLETE".to_string(),
7590 service_token: None,
7591 attributes: BTreeMap::new(),
7592 };
7593 assert_eq!(prov.get_att(&fresh, "RuleSetName"), None);
7594 }
7595
7596 #[test]
7597 fn ses_receipt_rule_lifecycle() {
7598 let prov = make_provisioner();
7599 let rs = make_resource(
7600 "AWS::SES::ReceiptRuleSet",
7601 "MyRuleSet",
7602 serde_json::json!({"RuleSetName": "my-rs2"}),
7603 );
7604 prov.create_resource(&rs).unwrap();
7605
7606 let res = make_resource(
7607 "AWS::SES::ReceiptRule",
7608 "MyRule",
7609 serde_json::json!({
7610 "RuleSetName": "my-rs2",
7611 "Rule": {
7612 "Name": "rule1",
7613 "Priority": 1,
7614 "Enabled": true,
7615 "Actions": [{"S3Action": {"BucketName": "my-bucket"}}],
7616 },
7617 }),
7618 );
7619 let sr = prov.create_resource(&res).unwrap();
7620 assert_eq!(sr.physical_id, "my-rs2|rule1");
7621
7622 prov.delete_resource(&sr.clone()).unwrap();
7623 }
7624
7625 #[test]
7626 fn ses_receipt_filter_lifecycle() {
7627 let prov = make_provisioner();
7628 let res = make_resource(
7629 "AWS::SES::ReceiptFilter",
7630 "MyFilter",
7631 serde_json::json!({
7632 "Filter": {
7633 "Name": "my-filter",
7634 "IpFilter": {"Policy": "Block", "Cidr": "10.0.0.0/8"},
7635 },
7636 }),
7637 );
7638 let sr = prov.create_resource(&res).unwrap();
7639 assert_eq!(sr.physical_id, "my-filter");
7640
7641 prov.delete_resource(&sr.clone()).unwrap();
7642 }
7643
7644 #[test]
7645 fn ses_vdm_attributes_lifecycle() {
7646 let prov = make_provisioner();
7647 let res = make_resource(
7648 "AWS::SES::VdmAttributes",
7649 "MyVdm",
7650 serde_json::json!({
7651 "DashboardAttributes": {"EngagementMetrics": "ENABLED"},
7652 "GuardianAttributes": {"OptimizedSharedDelivery": "ENABLED"},
7653 }),
7654 );
7655 let sr = prov.create_resource(&res).unwrap();
7656 assert_eq!(sr.physical_id, "vdm-MyVdm");
7657
7658 prov.delete_resource(&sr.clone()).unwrap();
7659 }
7660
7661 #[test]
7662 fn athena_work_group_lifecycle() {
7663 let prov = make_provisioner();
7664 let res = make_resource(
7665 "AWS::Athena::WorkGroup",
7666 "MyWg",
7667 serde_json::json!({
7668 "Name": "my-wg",
7669 "Description": "test wg",
7670 "Configuration": {"EnforceWorkGroupConfiguration": true},
7671 }),
7672 );
7673 let sr = prov.create_resource(&res).unwrap();
7674 assert_eq!(sr.physical_id, "my-wg");
7675 assert_eq!(sr.attributes.get("Name"), Some(&"my-wg".to_string()));
7676 assert!(sr
7677 .attributes
7678 .get("Arn")
7679 .unwrap()
7680 .contains("workgroup/my-wg"));
7681
7682 assert_eq!(
7683 prov.get_att(
7684 &StackResource {
7685 resource_type: "AWS::Athena::WorkGroup".to_string(),
7686 physical_id: sr.physical_id.clone(),
7687 logical_id: "MyWg".to_string(),
7688 status: "CREATE_COMPLETE".to_string(),
7689 service_token: None,
7690 attributes: BTreeMap::new(),
7691 },
7692 "Name",
7693 ),
7694 Some("my-wg".to_string()),
7695 );
7696
7697 prov.delete_resource(&sr.clone()).unwrap();
7698 }
7699
7700 #[test]
7701 fn athena_data_catalog_lifecycle() {
7702 let prov = make_provisioner();
7703 let res = make_resource(
7704 "AWS::Athena::DataCatalog",
7705 "MyCatalog",
7706 serde_json::json!({
7707 "Name": "my-catalog",
7708 "Type": "GLUE",
7709 "Description": "test catalog",
7710 }),
7711 );
7712 let sr = prov.create_resource(&res).unwrap();
7713 assert_eq!(sr.physical_id, "my-catalog");
7714 assert_eq!(sr.attributes.get("Name"), Some(&"my-catalog".to_string()));
7715 assert!(sr
7716 .attributes
7717 .get("Arn")
7718 .unwrap()
7719 .contains("datacatalog/my-catalog"));
7720
7721 prov.delete_resource(&sr.clone()).unwrap();
7722 }
7723
7724 #[test]
7725 fn athena_named_query_lifecycle() {
7726 let prov = make_provisioner();
7727 let res = make_resource(
7728 "AWS::Athena::NamedQuery",
7729 "MyQuery",
7730 serde_json::json!({
7731 "Name": "my-query",
7732 "Database": "mydb",
7733 "QueryString": "SELECT 1",
7734 "WorkGroup": "primary",
7735 }),
7736 );
7737 let sr = prov.create_resource(&res).unwrap();
7738 assert!(!sr.physical_id.is_empty());
7739 assert_eq!(sr.attributes.get("NamedQueryId"), Some(&sr.physical_id));
7740
7741 prov.delete_resource(&sr.clone()).unwrap();
7742 }
7743
7744 #[test]
7745 fn athena_prepared_statement_lifecycle() {
7746 let prov = make_provisioner();
7747 let res = make_resource(
7748 "AWS::Athena::PreparedStatement",
7749 "MyPs",
7750 serde_json::json!({
7751 "StatementName": "my-ps",
7752 "WorkGroupName": "primary",
7753 "QueryStatement": "SELECT 1",
7754 }),
7755 );
7756 let sr = prov.create_resource(&res).unwrap();
7757 assert_eq!(sr.physical_id, "primary|my-ps");
7758
7759 prov.delete_resource(&sr.clone()).unwrap();
7760 }
7761
7762 #[test]
7763 fn parse_lambda_function_name_handles_every_shape() {
7764 assert_eq!(parse_lambda_function_name("my-func"), "my-func");
7766 assert_eq!(parse_lambda_function_name("my-func:live"), "my-func");
7769 assert_eq!(parse_lambda_function_name("my-func:42"), "my-func");
7770 assert_eq!(
7772 parse_lambda_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-func"),
7773 "my-func"
7774 );
7775 assert_eq!(
7776 parse_lambda_function_name(
7777 "arn:aws:lambda:us-east-1:123456789012:function:my-func:live"
7778 ),
7779 "my-func"
7780 );
7781 assert_eq!(
7783 parse_lambda_function_name("123456789012:function:my-func"),
7784 "my-func"
7785 );
7786 assert_eq!(
7787 parse_lambda_function_name("123456789012:function:my-func:live"),
7788 "my-func"
7789 );
7790 }
7791
7792 #[test]
7793 fn alias_state_key_recovers_internal_key_from_arn() {
7794 assert_eq!(
7796 alias_state_key("arn:aws:lambda:us-east-1:123456789012:function:my-func:live"),
7797 "my-func:live"
7798 );
7799 assert_eq!(alias_state_key("my-func:live"), "my-func:live");
7801 }
7802}