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