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