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