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