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