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