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