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