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