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