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