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