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