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