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_cloudfront::{
30 functions::{
31 CloudFrontOriginAccessIdentityConfig, FunctionConfig, KeyGroupConfig, KeyGroupItems,
32 PublicKeyConfig, StoredFunction, StoredKeyGroup, StoredOriginAccessIdentity,
33 StoredPublicKey,
34 },
35 model::{
36 DefaultCacheBehavior, DistributionConfig, Origin, OriginItems, Origins, ViewerCertificate,
37 },
38 policies::{
39 CachePolicyConfig, OriginAccessControlConfig, OriginRequestPolicyConfig,
40 OriginRequestPolicyCookiesConfig, OriginRequestPolicyHeadersConfig,
41 OriginRequestPolicyQueryStringsConfig, ResponseHeadersPolicyConfig, StoredCachePolicy,
42 StoredOriginAccessControl, StoredOriginRequestPolicy, StoredResponseHeadersPolicy,
43 },
44 state::StoredDistribution,
45 SharedCloudFrontState,
46};
47use fakecloud_cloudwatch::{AlarmState, Dashboard, MetricAlarm, SharedCloudWatchState};
48use fakecloud_cognito::{
49 default_schema_attributes, AccountRecoverySetting, AdminCreateUserConfig,
50 CognitoIdentityProvider, CustomDomainConfig, EmailConfiguration, IdentityPool,
51 IdentityPoolRoleAttachment, PasswordPolicy, PoolPolicies, RecoveryOption, SchemaAttribute,
52 SharedCognitoState, SignInPolicy, SmsConfiguration, UserPool, UserPoolClient, UserPoolDomain,
53};
54use fakecloud_core::delivery::DeliveryBus;
55use fakecloud_dynamodb::{
56 AttributeDefinition, DynamoTable, KeySchemaElement, OnDemandThroughput, ProvisionedThroughput,
57 SharedDynamoDbState,
58};
59use fakecloud_ecr::{Repository, SharedEcrState};
60use fakecloud_ecs::{
61 CapacityProvider as EcsCapacityProvider, Cluster as EcsCluster, Service as EcsService,
62 SharedEcsState, TagEntry as EcsTagEntry, TaskDefinition as EcsTaskDefinition,
63};
64use fakecloud_elasticache::{
65 CacheCluster as EcCacheCluster, CacheParameterGroup, CacheSecurityGroup, CacheSubnetGroup,
66 ElastiCacheUser as EcUser, ElastiCacheUserGroup as EcUserGroup,
67 ReplicationGroup as EcReplicationGroup, SharedElastiCacheState,
68};
69use fakecloud_elbv2::{
70 Action as ElbAction, Listener, LoadBalancer, Rule as ElbRule, RuleCondition, SharedElbv2State,
71 Tag as ElbTag, TargetGroup, TargetGroupTuple,
72};
73use fakecloud_eventbridge::{
74 ApiDestination, Archive, Connection, Endpoint, EventBus, EventRule, SharedEventBridgeState,
75};
76use fakecloud_firehose::state::{DeliveryStream, S3Destination};
77use fakecloud_iam::{
78 IamAccessKey, IamGroup, IamInstanceProfile, IamPolicy, IamRole, IamUser, OidcProvider,
79 PolicyVersion, SamlProvider, SharedIamState, Tag, VirtualMfaDevice,
80};
81use fakecloud_kinesis::{build_stream_shards, KinesisConsumer, KinesisStream, SharedKinesisState};
82use fakecloud_kms::provisioner as kms_provisioner;
83use fakecloud_kms::SharedKmsState;
84use fakecloud_lambda::{
85 AttachedLayer, EventSourceMapping, FunctionAlias, FunctionUrlConfig, Layer, LayerVersion,
86 SharedLambdaState,
87};
88use fakecloud_logs::{
89 Delivery, DeliveryDestination, DeliverySource, Destination, LogStream, MetricFilter,
90 MetricTransformation, QueryDefinition, ResourcePolicy, SharedLogsState, SubscriptionFilter,
91};
92use fakecloud_organizations::{
93 OrganizationState, OrganizationalUnit, Policy as OrgPolicy, SharedOrganizationsState,
94 POLICY_TYPE_SCP,
95};
96use fakecloud_rds::{DbInstance, DbParameterGroup, DbSubnetGroup, RdsTag, SharedRdsState};
97use fakecloud_route53::{
98 model::{HealthCheckConfig, HostedZoneFeatures, ResourceRecordSet},
99 SharedRoute53State, StoredHealthCheck, StoredHostedZone,
100};
101use fakecloud_s3::{S3Bucket, SharedS3State};
102use fakecloud_secretsmanager::{RotationRules, Secret, SecretVersion, SharedSecretsManagerState};
103use fakecloud_ses::{
104 ConfigurationSet as SesConfigurationSet, ContactList as SesContactList,
105 DedicatedIpPool as SesDedicatedIpPool, EmailIdentity as SesEmailIdentity,
106 EmailTemplate as SesEmailTemplate, EventDestination as SesEventDestination,
107 IpFilter as SesIpFilter, ReceiptAction as SesReceiptAction, ReceiptFilter as SesReceiptFilter,
108 ReceiptRule as SesReceiptRule, ReceiptRuleSet as SesReceiptRuleSet, SharedSesState,
109};
110use fakecloud_sns::{SharedSnsState, SnsSubscription, SnsTopic};
111use fakecloud_sqs::{SharedSqsState, SqsQueue};
112use fakecloud_ssm::{SharedSsmState, SsmParameter};
113use fakecloud_stepfunctions::{
114 Activity as SfnActivity, AliasRoute, SharedStepFunctionsState, StateMachine, StateMachineAlias,
115 StateMachineStatus, StateMachineType, StateMachineVersion,
116};
117use fakecloud_wafv2::{IpSet, RegexPatternSet, RuleGroup, SharedWafv2State, WebAcl};
118
119use crate::state::StackResource;
120use crate::template::ResourceDefinition;
121
122fn parse_iam_tags(value: Option<&serde_json::Value>) -> Vec<Tag> {
126 let Some(arr) = value.and_then(|v| v.as_array()) else {
127 return Vec::new();
128 };
129 arr.iter()
130 .filter_map(|t| {
131 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
132 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
133 Some(Tag { key, value })
134 })
135 .collect()
136}
137
138fn parse_elb_tags(value: Option<&serde_json::Value>) -> Vec<ElbTag> {
141 let Some(arr) = value.and_then(|v| v.as_array()) else {
142 return Vec::new();
143 };
144 arr.iter()
145 .filter_map(|t| {
146 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
147 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
148 Some(ElbTag { key, value })
149 })
150 .collect()
151}
152
153fn parse_elb_actions(value: Option<&serde_json::Value>) -> Vec<ElbAction> {
157 let Some(arr) = value.and_then(|v| v.as_array()) else {
158 return Vec::new();
159 };
160 arr.iter()
161 .map(|a| {
162 let action_type = a
163 .get("Type")
164 .and_then(|v| v.as_str())
165 .unwrap_or("forward")
166 .to_string();
167 let target_group_arn = a
168 .get("TargetGroupArn")
169 .and_then(|v| v.as_str())
170 .map(|s| s.to_string());
171 let order = a.get("Order").and_then(|v| v.as_i64()).map(|n| n as i32);
172 let redirect = a
173 .get("RedirectConfig")
174 .map(|r| fakecloud_elbv2::RedirectConfig {
175 protocol: r
176 .get("Protocol")
177 .and_then(|v| v.as_str())
178 .map(|s| s.to_string()),
179 port: r
180 .get("Port")
181 .and_then(|v| v.as_str())
182 .map(|s| s.to_string()),
183 host: r
184 .get("Host")
185 .and_then(|v| v.as_str())
186 .map(|s| s.to_string()),
187 path: r
188 .get("Path")
189 .and_then(|v| v.as_str())
190 .map(|s| s.to_string()),
191 query: r
192 .get("Query")
193 .and_then(|v| v.as_str())
194 .map(|s| s.to_string()),
195 status_code: r
196 .get("StatusCode")
197 .and_then(|v| v.as_str())
198 .unwrap_or("HTTP_302")
199 .to_string(),
200 });
201 let fixed_response =
202 a.get("FixedResponseConfig")
203 .map(|f| fakecloud_elbv2::FixedResponseConfig {
204 message_body: f
205 .get("MessageBody")
206 .and_then(|v| v.as_str())
207 .map(|s| s.to_string()),
208 status_code: f
209 .get("StatusCode")
210 .and_then(|v| v.as_str())
211 .unwrap_or("200")
212 .to_string(),
213 content_type: f
214 .get("ContentType")
215 .and_then(|v| v.as_str())
216 .map(|s| s.to_string()),
217 });
218 let forward = a.get("ForwardConfig").map(|f| {
219 let target_groups: Vec<TargetGroupTuple> = f
220 .get("TargetGroups")
221 .and_then(|v| v.as_array())
222 .map(|arr| {
223 arr.iter()
224 .filter_map(|t| {
225 let target_group_arn = t
226 .get("TargetGroupArn")
227 .and_then(|v| v.as_str())?
228 .to_string();
229 let weight =
230 t.get("Weight").and_then(|v| v.as_i64()).map(|n| n as i32);
231 Some(TargetGroupTuple {
232 target_group_arn,
233 weight,
234 })
235 })
236 .collect()
237 })
238 .unwrap_or_default();
239 fakecloud_elbv2::ForwardConfig {
240 target_groups,
241 stickiness: None,
242 }
243 });
244 ElbAction {
245 action_type,
246 target_group_arn,
247 order,
248 redirect,
249 fixed_response,
250 forward,
251 authenticate_cognito: None,
252 authenticate_oidc: None,
253 }
254 })
255 .collect()
256}
257
258fn parse_elb_rule_conditions(value: Option<&serde_json::Value>) -> Vec<RuleCondition> {
259 let Some(arr) = value.and_then(|v| v.as_array()) else {
260 return Vec::new();
261 };
262 arr.iter()
263 .map(|c| {
264 let field = c
265 .get("Field")
266 .and_then(|v| v.as_str())
267 .unwrap_or("")
268 .to_string();
269 let values: Vec<String> = c
270 .get("Values")
271 .and_then(|v| v.as_array())
272 .map(|arr| {
273 arr.iter()
274 .filter_map(|s| s.as_str().map(|s| s.to_string()))
275 .collect()
276 })
277 .unwrap_or_default();
278 let host_header_values: Vec<String> = c
279 .get("HostHeaderConfig")
280 .and_then(|v| v.get("Values"))
281 .and_then(|v| v.as_array())
282 .map(|arr| {
283 arr.iter()
284 .filter_map(|s| s.as_str().map(|s| s.to_string()))
285 .collect()
286 })
287 .unwrap_or_default();
288 RuleCondition {
289 field,
290 values,
291 host_header_values,
292 path_pattern_values: Vec::new(),
293 http_header_name: None,
294 http_header_values: Vec::new(),
295 query_string_values: Vec::new(),
296 http_request_method_values: Vec::new(),
297 source_ip_values: Vec::new(),
298 }
299 })
300 .collect()
301}
302
303fn parse_key_policy(props: &serde_json::Value) -> Option<String> {
308 match props.get("KeyPolicy") {
309 Some(v) if v.is_string() => Some(v.as_str().unwrap_or("").to_string()),
310 Some(v) => Some(serde_json::to_string(v).unwrap_or_default()),
311 None => None,
312 }
313}
314
315fn parse_tag_list(props: &serde_json::Value) -> BTreeMap<String, String> {
318 let mut tags: BTreeMap<String, String> = BTreeMap::new();
319 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
320 for t in arr {
321 if let (Some(k), Some(v)) = (
322 t.get("Key").and_then(|x| x.as_str()),
323 t.get("Value").and_then(|x| x.as_str()),
324 ) {
325 tags.insert(k.to_string(), v.to_string());
326 }
327 }
328 }
329 tags
330}
331
332fn parse_kms_key_input(props: &serde_json::Value) -> kms_provisioner::KeyCreationInput {
342 kms_provisioner::KeyCreationInput {
343 description: props
344 .get("Description")
345 .and_then(|v| v.as_str())
346 .unwrap_or("")
347 .to_string(),
348 key_usage: props
349 .get("KeyUsage")
350 .and_then(|v| v.as_str())
351 .unwrap_or("ENCRYPT_DECRYPT")
352 .to_string(),
353 key_spec: props
354 .get("KeySpec")
355 .and_then(|v| v.as_str())
356 .unwrap_or("SYMMETRIC_DEFAULT")
357 .to_string(),
358 origin: props
359 .get("Origin")
360 .and_then(|v| v.as_str())
361 .unwrap_or("AWS_KMS")
362 .to_string(),
363 enabled: props
364 .get("Enabled")
365 .and_then(|v| v.as_bool())
366 .unwrap_or(true),
367 multi_region: props
368 .get("MultiRegion")
369 .and_then(|v| v.as_bool())
370 .unwrap_or(false),
371 key_rotation_enabled: props
372 .get("EnableKeyRotation")
373 .and_then(|v| v.as_bool())
374 .unwrap_or(false),
375 policy: parse_key_policy(props),
376 tags: parse_tag_list(props),
377 }
378}
379
380fn parse_log_group_name(input: &str) -> String {
384 if let Some(rest) = input.strip_prefix("arn:aws:logs:") {
385 if let Some(after) = rest.split(":log-group:").nth(1) {
386 return after.trim_end_matches(":*").to_string();
388 }
389 }
390 input.to_string()
391}
392
393fn parse_lambda_function_name(input: &str) -> String {
398 if let Some(rest) = input.strip_prefix("arn:aws:lambda:") {
399 if let Some(after) = rest.split(":function:").nth(1) {
400 return after.split(':').next().unwrap_or(after).to_string();
402 }
403 }
404 input.to_string()
405}
406
407struct LambdaFunctionProps {
411 runtime: String,
412 role: String,
413 handler: String,
414 description: String,
415 timeout: i64,
416 memory_size: i64,
417 package_type: String,
418 tags: BTreeMap<String, String>,
419 environment: BTreeMap<String, String>,
420 architectures: Vec<String>,
421 code_zip: Option<Vec<u8>>,
425 s3_bucket: Option<String>,
426 s3_key: Option<String>,
427 image_uri: Option<String>,
428 layers: Vec<String>,
429 tracing_mode: Option<String>,
430 kms_key_arn: Option<String>,
431 ephemeral_storage_size: Option<i64>,
432 vpc_config: Option<serde_json::Value>,
433 snap_start: Option<serde_json::Value>,
434 dead_letter_config_arn: Option<String>,
435 file_system_configs: Vec<serde_json::Value>,
436 logging_config: Option<serde_json::Value>,
437}
438
439fn parse_lambda_function_props(props: &serde_json::Value) -> Result<LambdaFunctionProps, String> {
444 let runtime = props
445 .get("Runtime")
446 .and_then(|v| v.as_str())
447 .unwrap_or("python3.12")
448 .to_string();
449 let role = props
450 .get("Role")
451 .and_then(|v| v.as_str())
452 .unwrap_or_default()
453 .to_string();
454 let handler = props
455 .get("Handler")
456 .and_then(|v| v.as_str())
457 .unwrap_or("index.handler")
458 .to_string();
459 let description = props
460 .get("Description")
461 .and_then(|v| v.as_str())
462 .unwrap_or_default()
463 .to_string();
464 let timeout = props.get("Timeout").and_then(|v| v.as_i64()).unwrap_or(3);
465 let memory_size = props
466 .get("MemorySize")
467 .and_then(|v| v.as_i64())
468 .unwrap_or(128);
469 let architectures = props
470 .get("Architectures")
471 .and_then(|v| v.as_array())
472 .map(|a| {
473 a.iter()
474 .filter_map(|v| v.as_str().map(|s| s.to_string()))
475 .collect::<Vec<_>>()
476 })
477 .unwrap_or_else(|| vec!["x86_64".to_string()]);
478 let package_type = props
479 .get("PackageType")
480 .and_then(|v| v.as_str())
481 .unwrap_or("Zip")
482 .to_string();
483 let environment = props
484 .get("Environment")
485 .and_then(|v| v.get("Variables"))
486 .and_then(|v| v.as_object())
487 .map(|o| {
488 o.iter()
489 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
490 .collect::<BTreeMap<String, String>>()
491 })
492 .unwrap_or_default();
493
494 let tags: BTreeMap<String, String> = props
497 .get("Tags")
498 .and_then(|v| v.as_array())
499 .map(|arr| {
500 arr.iter()
501 .filter_map(|t| {
502 let k = t.get("Key").and_then(|v| v.as_str())?.to_string();
503 let v = t.get("Value").and_then(|v| v.as_str())?.to_string();
504 Some((k, v))
505 })
506 .collect()
507 })
508 .unwrap_or_default();
509
510 let code = props.get("Code");
511 let code_zip = code
517 .and_then(|c| c.get("ZipFile"))
518 .and_then(|v| v.as_str())
519 .map(|s| s.as_bytes().to_vec());
520 let s3_bucket = code
521 .and_then(|c| c.get("S3Bucket"))
522 .and_then(|v| v.as_str())
523 .map(|s| s.to_string());
524 let s3_key = code
525 .and_then(|c| c.get("S3Key"))
526 .and_then(|v| v.as_str())
527 .map(|s| s.to_string());
528 let image_uri = if package_type == "Image" {
532 code.and_then(|c| c.get("ImageUri"))
533 .and_then(|v| v.as_str())
534 .map(|s| s.to_string())
535 } else {
536 None
537 };
538 if package_type == "Image" && image_uri.is_none() {
539 return Err("Code.ImageUri is required when PackageType is Image".to_string());
540 }
541
542 let layers: Vec<String> = props
543 .get("Layers")
544 .and_then(|v| v.as_array())
545 .map(|arr| {
546 arr.iter()
547 .filter_map(|v| v.as_str().map(String::from))
548 .collect()
549 })
550 .unwrap_or_default();
551
552 let tracing_mode = props
553 .get("TracingConfig")
554 .and_then(|v| v.get("Mode"))
555 .and_then(|v| v.as_str())
556 .map(String::from);
557 let kms_key_arn = props
558 .get("KmsKeyArn")
559 .and_then(|v| v.as_str())
560 .map(String::from);
561 let ephemeral_storage_size = props
562 .get("EphemeralStorage")
563 .and_then(|v| v.get("Size"))
564 .and_then(|v| v.as_i64());
565 let vpc_config = props.get("VpcConfig").filter(|v| v.is_object()).cloned();
566 let snap_start = props.get("SnapStart").filter(|v| v.is_object()).cloned();
567 let dead_letter_config_arn = props
568 .get("DeadLetterConfig")
569 .and_then(|v| v.get("TargetArn"))
570 .and_then(|v| v.as_str())
571 .map(String::from);
572 let file_system_configs = props
573 .get("FileSystemConfigs")
574 .and_then(|v| v.as_array())
575 .cloned()
576 .unwrap_or_default();
577 let logging_config = props
578 .get("LoggingConfig")
579 .filter(|v| v.is_object())
580 .cloned();
581
582 Ok(LambdaFunctionProps {
583 runtime,
584 role,
585 handler,
586 description,
587 timeout,
588 memory_size,
589 package_type,
590 tags,
591 environment,
592 architectures,
593 code_zip,
594 s3_bucket,
595 s3_key,
596 image_uri,
597 layers,
598 tracing_mode,
599 kms_key_arn,
600 ephemeral_storage_size,
601 vpc_config,
602 snap_start,
603 dead_letter_config_arn,
604 file_system_configs,
605 logging_config,
606 })
607}
608
609struct LambdaEventSourceMappingProps {
614 event_source_arn: String,
615 batch_size: i64,
616 enabled: bool,
617 starting_position: Option<String>,
618 starting_position_timestamp: Option<f64>,
619 parallelization_factor: Option<i64>,
620 maximum_batching_window_in_seconds: Option<i64>,
621 function_response_types: Vec<String>,
622 filter_patterns: Vec<String>,
623 kms_key_arn: Option<String>,
624 metrics_config: Option<serde_json::Value>,
625 destination_config: Option<serde_json::Value>,
626 maximum_retry_attempts: Option<i64>,
627 maximum_record_age_in_seconds: Option<i64>,
628 bisect_batch_on_function_error: Option<bool>,
629 tumbling_window_in_seconds: Option<i64>,
630 topics: Vec<String>,
631 queues: Vec<String>,
632}
633
634fn parse_lambda_event_source_mapping_props(
638 props: &serde_json::Value,
639) -> Result<LambdaEventSourceMappingProps, String> {
640 let event_source_arn = props
641 .get("EventSourceArn")
642 .and_then(|v| v.as_str())
643 .unwrap_or_default()
644 .to_string();
645 let batch_size = props
646 .get("BatchSize")
647 .and_then(|v| v.as_i64())
648 .unwrap_or(10);
649 let enabled = props
650 .get("Enabled")
651 .and_then(|v| v.as_bool())
652 .unwrap_or(true);
653 let starting_position = props
654 .get("StartingPosition")
655 .and_then(|v| v.as_str())
656 .map(|s| s.to_string());
657 let starting_position_timestamp = props
658 .get("StartingPositionTimestamp")
659 .and_then(|v| v.as_f64());
660 let parallelization_factor = props.get("ParallelizationFactor").and_then(|v| v.as_i64());
661 let maximum_batching_window_in_seconds = props
662 .get("MaximumBatchingWindowInSeconds")
663 .and_then(|v| v.as_i64());
664 let function_response_types: Vec<String> = props
665 .get("FunctionResponseTypes")
666 .and_then(|v| v.as_array())
667 .map(|arr| {
668 arr.iter()
669 .filter_map(|v| v.as_str().map(|s| s.to_string()))
670 .collect()
671 })
672 .unwrap_or_default();
673 let filter_patterns: Vec<String> = props
674 .get("FilterCriteria")
675 .and_then(|v| v.get("Filters"))
676 .and_then(|v| v.as_array())
677 .map(|arr| {
678 arr.iter()
679 .filter_map(|f| {
680 f.get("Pattern")
681 .and_then(|p| p.as_str())
682 .map(|s| s.to_string())
683 })
684 .collect()
685 })
686 .unwrap_or_default();
687 let kms_key_arn = props
688 .get("KmsKeyArn")
689 .and_then(|v| v.as_str())
690 .map(|s| s.to_string());
691 let metrics_config = props
692 .get("MetricsConfig")
693 .filter(|v| v.is_object())
694 .cloned();
695 let destination_config = props
696 .get("DestinationConfig")
697 .filter(|v| v.is_object())
698 .cloned();
699 let maximum_retry_attempts = props.get("MaximumRetryAttempts").and_then(|v| v.as_i64());
700 let maximum_record_age_in_seconds = props
701 .get("MaximumRecordAgeInSeconds")
702 .and_then(|v| v.as_i64());
703 let bisect_batch_on_function_error = props
704 .get("BisectBatchOnFunctionError")
705 .and_then(|v| v.as_bool());
706 let tumbling_window_in_seconds = props
707 .get("TumblingWindowInSeconds")
708 .and_then(|v| v.as_i64());
709 let topics: Vec<String> = props
710 .get("Topics")
711 .and_then(|v| v.as_array())
712 .map(|arr| {
713 arr.iter()
714 .filter_map(|v| v.as_str().map(|s| s.to_string()))
715 .collect()
716 })
717 .unwrap_or_default();
718 let queues: Vec<String> = props
719 .get("Queues")
720 .and_then(|v| v.as_array())
721 .map(|arr| {
722 arr.iter()
723 .filter_map(|v| v.as_str().map(|s| s.to_string()))
724 .collect()
725 })
726 .unwrap_or_default();
727
728 Ok(LambdaEventSourceMappingProps {
729 event_source_arn,
730 batch_size,
731 enabled,
732 starting_position,
733 starting_position_timestamp,
734 parallelization_factor,
735 maximum_batching_window_in_seconds,
736 function_response_types,
737 filter_patterns,
738 kms_key_arn,
739 metrics_config,
740 destination_config,
741 maximum_retry_attempts,
742 maximum_record_age_in_seconds,
743 bisect_batch_on_function_error,
744 tumbling_window_in_seconds,
745 topics,
746 queues,
747 })
748}
749
750fn sha256_b64(bytes: &[u8]) -> String {
753 use sha2::Digest;
754 let hash = sha2::Sha256::digest(bytes);
755 base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash)
756}
757
758fn layer_code_size(
763 accounts: &fakecloud_core::multi_account::MultiAccountState<fakecloud_lambda::LambdaState>,
764 arn: &str,
765) -> i64 {
766 let Some(rest) = arn.strip_prefix("arn:aws:lambda:") else {
768 return 0;
769 };
770 let mut parts = rest.split(':');
771 let _region = parts.next();
772 let Some(account) = parts.next() else {
773 return 0;
774 };
775 if parts.next() != Some("layer") {
776 return 0;
777 }
778 let Some(name) = parts.next() else {
779 return 0;
780 };
781 let Some(ver_str) = parts.next() else {
782 return 0;
783 };
784 let Ok(ver) = ver_str.parse::<i64>() else {
785 return 0;
786 };
787 accounts
788 .get(account)
789 .and_then(|s| s.layers.get(name))
790 .and_then(|l| l.versions.iter().find(|v| v.version == ver))
791 .map(|v| v.code_size)
792 .unwrap_or(0)
793}
794
795pub struct ProvisionResult {
798 pub physical_id: String,
799 pub attributes: BTreeMap<String, String>,
800}
801
802impl ProvisionResult {
803 pub fn new(physical_id: impl Into<String>) -> Self {
804 Self {
805 physical_id: physical_id.into(),
806 attributes: BTreeMap::new(),
807 }
808 }
809
810 pub fn with(mut self, key: &str, value: impl Into<String>) -> Self {
811 self.attributes.insert(key.to_string(), value.into());
812 self
813 }
814
815 pub fn merge_attributes(mut self, other: BTreeMap<String, String>) -> Self {
820 for (k, v) in other {
821 self.attributes.insert(k, v);
822 }
823 self
824 }
825}
826
827pub struct ResourceProvisioner {
829 pub sqs_state: SharedSqsState,
830 pub sns_state: SharedSnsState,
831 pub ssm_state: SharedSsmState,
832 pub iam_state: SharedIamState,
833 pub s3_state: SharedS3State,
834 pub eventbridge_state: SharedEventBridgeState,
835 pub dynamodb_state: SharedDynamoDbState,
836 pub logs_state: SharedLogsState,
837 pub lambda_state: SharedLambdaState,
838 pub secretsmanager_state: SharedSecretsManagerState,
839 pub kinesis_state: SharedKinesisState,
840 pub kms_state: SharedKmsState,
841 pub ecr_state: SharedEcrState,
842 pub cloudwatch_state: SharedCloudWatchState,
843 pub elbv2_state: SharedElbv2State,
844 pub organizations_state: SharedOrganizationsState,
845 pub cognito_state: SharedCognitoState,
846 pub rds_state: SharedRdsState,
847 pub ecs_state: SharedEcsState,
848 pub acm_state: SharedAcmState,
849 pub elasticache_state: SharedElastiCacheState,
850 pub route53_state: SharedRoute53State,
851 pub cloudfront_state: SharedCloudFrontState,
852 pub stepfunctions_state: SharedStepFunctionsState,
853 pub wafv2_state: SharedWafv2State,
854 pub apigateway_state: SharedApiGatewayState,
855 pub apigatewayv2_state: SharedApiGatewayV2State,
856 pub ses_state: SharedSesState,
857 pub app_autoscaling_state: AppasState,
858 pub athena_state: SharedAthenaState,
859 pub firehose_state: fakecloud_firehose::SharedFirehoseState,
860 pub glue_state: fakecloud_glue::SharedGlueState,
861 pub cloudformation_state: SharedCloudFormationState,
862 pub delivery: Arc<DeliveryBus>,
863 pub account_id: String,
864 pub region: String,
865 pub stack_id: String,
866}
867
868impl ResourceProvisioner {
869 pub fn create_resource(&self, resource: &ResourceDefinition) -> Result<StackResource, String> {
871 let result = match resource.resource_type.as_str() {
872 "AWS::SQS::Queue" => self.create_sqs_queue(resource),
873 "AWS::SNS::Topic" => self.create_sns_topic(resource),
874 "AWS::SNS::Subscription" => self.create_sns_subscription(resource),
875 "AWS::SSM::Parameter" => self.create_ssm_parameter(resource),
876 "AWS::IAM::Role" => self.create_iam_role(resource),
877 "AWS::IAM::Policy" => self.create_iam_policy(resource),
878 "AWS::IAM::User" => self.create_iam_user(resource),
879 "AWS::IAM::Group" => self.create_iam_group(resource),
880 "AWS::IAM::ManagedPolicy" => self.create_iam_managed_policy(resource),
881 "AWS::IAM::UserToGroupAddition" => self.create_iam_user_to_group_addition(resource),
882 "AWS::IAM::AccessKey" => self.create_iam_access_key(resource),
883 "AWS::IAM::InstanceProfile" => self.create_iam_instance_profile(resource),
884 "AWS::IAM::OIDCProvider" => self.create_iam_oidc_provider(resource),
885 "AWS::IAM::SAMLProvider" => self.create_iam_saml_provider(resource),
886 "AWS::IAM::ServiceLinkedRole" => self.create_iam_service_linked_role(resource),
887 "AWS::IAM::VirtualMFADevice" => self.create_iam_virtual_mfa_device(resource),
888 "AWS::S3::Bucket" => self.create_s3_bucket(resource),
889 "AWS::Events::Rule" => self.create_eventbridge_rule(resource),
890 "AWS::Events::Connection" => self.create_eventbridge_connection(resource),
891 "AWS::Events::ApiDestination" => self.create_eventbridge_api_destination(resource),
892 "AWS::Events::Archive" => self.create_eventbridge_archive(resource),
893 "AWS::Events::EventBus" => self.create_eventbridge_event_bus(resource),
894 "AWS::Events::EventBusPolicy" => self.create_eventbridge_event_bus_policy(resource),
895 "AWS::Events::Endpoint" => self.create_eventbridge_endpoint(resource),
896 "AWS::DynamoDB::Table" => self.create_dynamodb_table(resource),
897 "AWS::Logs::LogGroup" => self.create_log_group(resource),
898 "AWS::Logs::LogStream" => self.create_log_stream(resource),
899 "AWS::Logs::MetricFilter" => self.create_metric_filter(resource),
900 "AWS::Logs::SubscriptionFilter" => self.create_subscription_filter(resource),
901 "AWS::Logs::Destination" => self.create_logs_destination(resource),
902 "AWS::Logs::ResourcePolicy" => self.create_logs_resource_policy(resource),
903 "AWS::Logs::QueryDefinition" => self.create_logs_query_definition(resource),
904 "AWS::Logs::Delivery" => self.create_logs_delivery(resource),
905 "AWS::Logs::DeliveryDestination" => self.create_logs_delivery_destination(resource),
906 "AWS::Logs::DeliverySource" => self.create_logs_delivery_source(resource),
907 "AWS::Lambda::Function" => self.create_lambda_function(resource),
908 "AWS::Lambda::Permission" => self.create_lambda_permission(resource),
909 "AWS::Lambda::EventSourceMapping" => self.create_lambda_event_source_mapping(resource),
910 "AWS::Lambda::LayerVersion" => self.create_lambda_layer_version(resource),
911 "AWS::Lambda::Url" => self.create_lambda_url(resource),
912 "AWS::Lambda::Alias" => self.create_lambda_alias(resource),
913 "AWS::Lambda::Version" => self.create_lambda_version(resource),
914 "AWS::SecretsManager::Secret" => self.create_secrets_manager_secret(resource),
915 "AWS::Kinesis::Stream" => self.create_kinesis_stream(resource),
916 "AWS::Kinesis::StreamConsumer" => self.create_kinesis_stream_consumer(resource),
917 "AWS::KMS::Key" => self.create_kms_key(resource),
918 "AWS::KMS::Alias" => self.create_kms_alias(resource),
919 "AWS::KMS::ReplicaKey" => self.create_kms_replica_key(resource),
920 "AWS::ECR::Repository" => self.create_ecr_repository(resource),
921 "AWS::ECR::RepositoryPolicy" => self.create_ecr_repository_policy(resource),
922 "AWS::ECR::LifecyclePolicy" => self.create_ecr_lifecycle_policy(resource),
923 "AWS::ECR::RegistryPolicy" => self.create_ecr_registry_policy(resource),
924 "AWS::ECR::ReplicationConfiguration" => {
925 self.create_ecr_replication_configuration(resource)
926 }
927 "AWS::ECR::RegistryScanningConfiguration" => {
928 self.create_ecr_registry_scanning_configuration(resource)
929 }
930 "AWS::ECR::PullThroughCacheRule" => self.create_ecr_pull_through_cache_rule(resource),
931 "AWS::CloudWatch::Alarm" => self.create_cloudwatch_alarm(resource),
932 "AWS::CloudWatch::Dashboard" => self.create_cloudwatch_dashboard(resource),
933 "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
934 self.create_elbv2_load_balancer(resource)
935 }
936 "AWS::ElasticLoadBalancingV2::TargetGroup" => self.create_elbv2_target_group(resource),
937 "AWS::ElasticLoadBalancingV2::Listener" => self.create_elbv2_listener(resource),
938 "AWS::ElasticLoadBalancingV2::ListenerRule" => {
939 self.create_elbv2_listener_rule(resource)
940 }
941 "AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
942 self.create_elbv2_listener_certificate(resource)
943 }
944 "AWS::ElasticLoadBalancingV2::TrustStore" => self.create_elbv2_trust_store(resource),
945 "AWS::Organizations::Organization" => self.create_organization(resource),
946 "AWS::Organizations::OrganizationalUnit" => self.create_organization_unit(resource),
947 "AWS::Organizations::Account" => self.create_organization_account(resource),
948 "AWS::Organizations::Policy" => self.create_organization_policy(resource),
949 "AWS::Organizations::ResourcePolicy" => {
950 self.create_organization_resource_policy(resource)
951 }
952 "AWS::Cognito::UserPool" => self.create_cognito_user_pool(resource),
953 "AWS::Cognito::UserPoolClient" => self.create_cognito_user_pool_client(resource),
954 "AWS::Cognito::UserPoolDomain" => self.create_cognito_user_pool_domain(resource),
955 "AWS::Cognito::IdentityPool" => self.create_cognito_identity_pool(resource),
956 "AWS::Cognito::IdentityPoolRoleAttachment" => {
957 self.create_cognito_identity_pool_role_attachment(resource)
958 }
959 "AWS::RDS::DBSubnetGroup" => self.create_rds_subnet_group(resource),
960 "AWS::RDS::DBParameterGroup" => self.create_rds_parameter_group(resource),
961 "AWS::RDS::DBClusterParameterGroup" => {
962 self.create_rds_cluster_parameter_group(resource)
963 }
964 "AWS::RDS::OptionGroup" => self.create_rds_option_group(resource),
965 "AWS::RDS::EventSubscription" => self.create_rds_event_subscription(resource),
966 "AWS::RDS::DBSecurityGroup" => self.create_rds_security_group(resource),
967 "AWS::RDS::DBProxy" => self.create_rds_db_proxy(resource),
968 "AWS::RDS::DBInstance" => self.create_rds_db_instance(resource),
969 "AWS::RDS::DBCluster" => self.create_rds_db_cluster(resource),
970 "AWS::ECS::Cluster" => self.create_ecs_cluster(resource),
971 "AWS::ECS::TaskDefinition" => self.create_ecs_task_definition(resource),
972 "AWS::ECS::Service" => self.create_ecs_service(resource),
973 "AWS::ECS::CapacityProvider" => self.create_ecs_capacity_provider(resource),
974 "AWS::CertificateManager::Certificate" => self.create_acm_certificate(resource),
975 "AWS::CertificateManager::Account" => self.create_acm_account(resource),
976 "AWS::ElastiCache::ParameterGroup" => self.create_ec_parameter_group(resource),
977 "AWS::ElastiCache::SubnetGroup" => self.create_ec_subnet_group(resource),
978 "AWS::ElastiCache::SecurityGroup" => self.create_ec_security_group(resource),
979 "AWS::ElastiCache::User" => self.create_ec_user(resource),
980 "AWS::ElastiCache::UserGroup" => self.create_ec_user_group(resource),
981 "AWS::ElastiCache::CacheCluster" => self.create_ec_cache_cluster(resource),
982 "AWS::ElastiCache::ReplicationGroup" => self.create_ec_replication_group(resource),
983 "AWS::Route53::HostedZone" => self.create_route53_hosted_zone(resource),
984 "AWS::Route53::RecordSet" => self.create_route53_record_set(resource),
985 "AWS::Route53::HealthCheck" => self.create_route53_health_check(resource),
986 "AWS::Route53::DNSSEC" => self.create_route53_dnssec(resource),
987 "AWS::Route53::KeySigningKey" => self.create_route53_key_signing_key(resource),
988 "AWS::CloudFront::CloudFrontOriginAccessIdentity" => {
989 self.create_cf_origin_access_identity(resource)
990 }
991 "AWS::CloudFront::Distribution" => self.create_cf_distribution(resource),
992 "AWS::CloudFront::OriginAccessControl" => {
993 self.create_cf_origin_access_control(resource)
994 }
995 "AWS::CloudFront::PublicKey" => self.create_cf_public_key(resource),
996 "AWS::CloudFront::KeyGroup" => self.create_cf_key_group(resource),
997 "AWS::CloudFront::Function" => self.create_cf_function(resource),
998 "AWS::CloudFront::CachePolicy" => self.create_cf_cache_policy(resource),
999 "AWS::CloudFront::OriginRequestPolicy" => {
1000 self.create_cf_origin_request_policy(resource)
1001 }
1002 "AWS::CloudFront::ResponseHeadersPolicy" => {
1003 self.create_cf_response_headers_policy(resource)
1004 }
1005 "AWS::StepFunctions::StateMachine" => self.create_sfn_state_machine(resource),
1006 "AWS::StepFunctions::Activity" => self.create_sfn_activity(resource),
1007 "AWS::StepFunctions::StateMachineVersion" => self.create_sfn_version(resource),
1008 "AWS::StepFunctions::StateMachineAlias" => self.create_sfn_alias(resource),
1009 "AWS::WAFv2::WebACL" => self.create_wafv2_web_acl(resource),
1010 "AWS::WAFv2::IPSet" => self.create_wafv2_ip_set(resource),
1011 "AWS::WAFv2::RegexPatternSet" => self.create_wafv2_regex_pattern_set(resource),
1012 "AWS::WAFv2::RuleGroup" => self.create_wafv2_rule_group(resource),
1013 "AWS::WAFv2::LoggingConfiguration" => self.create_wafv2_logging_configuration(resource),
1014 "AWS::WAFv2::WebACLAssociation" => self.create_wafv2_web_acl_association(resource),
1015 "AWS::ApiGateway::RestApi" => self.create_apigw_rest_api(resource),
1016 "AWS::ApiGateway::Resource" => self.create_apigw_resource(resource),
1017 "AWS::ApiGateway::Method" => self.create_apigw_method(resource),
1018 "AWS::ApiGateway::Deployment" => self.create_apigw_deployment(resource),
1019 "AWS::ApiGateway::Stage" => self.create_apigw_stage(resource),
1020 "AWS::ApiGateway::Authorizer" => self.create_apigw_authorizer(resource),
1021 "AWS::ApiGateway::RequestValidator" => self.create_apigw_request_validator(resource),
1022 "AWS::ApiGateway::Model" => self.create_apigw_model(resource),
1023 "AWS::ApiGateway::GatewayResponse" => self.create_apigw_gateway_response(resource),
1024 "AWS::ApiGateway::UsagePlan" => self.create_apigw_usage_plan(resource),
1025 "AWS::ApiGateway::ApiKey" => self.create_apigw_api_key(resource),
1026 "AWS::ApiGateway::UsagePlanKey" => self.create_apigw_usage_plan_key(resource),
1027 "AWS::ApiGateway::DomainName" => self.create_apigw_domain_name(resource),
1028 "AWS::ApiGateway::BasePathMapping" => self.create_apigw_base_path_mapping(resource),
1029 "AWS::ApiGatewayV2::Api" => self.create_apigwv2_api(resource),
1030 "AWS::ApiGatewayV2::Route" => self.create_apigwv2_route(resource),
1031 "AWS::ApiGatewayV2::Integration" => self.create_apigwv2_integration(resource),
1032 "AWS::ApiGatewayV2::IntegrationResponse" => {
1033 self.create_apigwv2_integration_response(resource)
1034 }
1035 "AWS::ApiGatewayV2::RouteResponse" => self.create_apigwv2_route_response(resource),
1036 "AWS::ApiGatewayV2::Stage" => self.create_apigwv2_stage(resource),
1037 "AWS::ApiGatewayV2::Deployment" => self.create_apigwv2_deployment(resource),
1038 "AWS::ApiGatewayV2::Authorizer" => self.create_apigwv2_authorizer(resource),
1039 "AWS::ApiGatewayV2::DomainName" => self.create_apigwv2_domain_name(resource),
1040 "AWS::ApiGatewayV2::ApiMapping" => self.create_apigwv2_api_mapping(resource),
1041 "AWS::ApiGatewayV2::VpcLink" => self.create_apigwv2_vpc_link(resource),
1042 "AWS::ApiGatewayV2::Model" => self.create_apigwv2_model(resource),
1043 "AWS::SES::ConfigurationSet" => self.create_ses_configuration_set(resource),
1044 "AWS::SES::ConfigurationSetEventDestination" => {
1045 self.create_ses_event_destination(resource)
1046 }
1047 "AWS::SES::EmailIdentity" => self.create_ses_email_identity(resource),
1048 "AWS::SES::Template" => self.create_ses_template(resource),
1049 "AWS::SES::ContactList" => self.create_ses_contact_list(resource),
1050 "AWS::SES::DedicatedIpPool" => self.create_ses_dedicated_ip_pool(resource),
1051 "AWS::SES::ReceiptRule" => self.create_ses_receipt_rule(resource),
1052 "AWS::SES::ReceiptRuleSet" => self.create_ses_receipt_rule_set(resource),
1053 "AWS::SES::ReceiptFilter" => self.create_ses_receipt_filter(resource),
1054 "AWS::SES::VdmAttributes" => self.create_ses_vdm_attributes(resource),
1055 "AWS::SecretsManager::RotationSchedule" => {
1056 self.create_secrets_manager_rotation_schedule(resource)
1057 }
1058 "AWS::SecretsManager::ResourcePolicy" => {
1059 self.create_secrets_manager_resource_policy(resource)
1060 }
1061 "AWS::SecretsManager::SecretTargetAttachment" => {
1062 self.create_secrets_manager_target_attachment(resource)
1063 }
1064 "AWS::ApplicationAutoScaling::ScalableTarget" => {
1065 self.create_application_autoscaling_scalable_target(resource)
1066 }
1067 "AWS::ApplicationAutoScaling::ScalingPolicy" => {
1068 self.create_application_autoscaling_scaling_policy(resource)
1069 }
1070 "AWS::Athena::DataCatalog" => self.create_athena_data_catalog(resource),
1071 "AWS::Athena::NamedQuery" => self.create_athena_named_query(resource),
1072 "AWS::Athena::WorkGroup" => self.create_athena_work_group(resource),
1073 "AWS::Athena::PreparedStatement" => self.create_athena_prepared_statement(resource),
1074 "AWS::KinesisFirehose::DeliveryStream" => {
1075 self.create_firehose_delivery_stream(resource)
1076 }
1077 "AWS::Glue::Database" => self.create_glue_database(resource),
1078 "AWS::CloudFormation::Stack" => self.create_cloudformation_stack(resource),
1079 "AWS::Glue::Table" => self.create_glue_table(resource),
1080 "AWS::Glue::Partition" => self.create_glue_partition(resource),
1081 t if t.starts_with("Custom::") || t == "AWS::CloudFormation::CustomResource" => self
1082 .create_custom_resource(resource)
1083 .map(ProvisionResult::new),
1084 other => Err(format!("Unsupported resource type: {other}")),
1085 };
1086
1087 let is_custom = resource.resource_type.starts_with("Custom::")
1088 || resource.resource_type == "AWS::CloudFormation::CustomResource";
1089 let service_token = if is_custom {
1090 resource
1091 .properties
1092 .get("ServiceToken")
1093 .and_then(|v| v.as_str())
1094 .map(|s| s.to_string())
1095 } else {
1096 None
1097 };
1098
1099 result.map(|res| StackResource {
1100 logical_id: resource.logical_id.clone(),
1101 physical_id: res.physical_id,
1102 resource_type: resource.resource_type.clone(),
1103 status: "CREATE_COMPLETE".to_string(),
1104 service_token,
1105 attributes: res.attributes,
1106 })
1107 }
1108
1109 pub fn update_resource(
1116 &self,
1117 existing: &StackResource,
1118 new_def: &ResourceDefinition,
1119 ) -> Result<Option<StackResource>, String> {
1120 let result = match new_def.resource_type.as_str() {
1121 "AWS::Lambda::Function" => Some(self.update_lambda_function(existing, new_def)?),
1122 "AWS::Lambda::Permission" => Some(self.update_lambda_permission(existing, new_def)?),
1123 "AWS::Lambda::EventSourceMapping" => {
1124 Some(self.update_lambda_event_source_mapping(existing, new_def)?)
1125 }
1126 "AWS::Lambda::LayerVersion" => {
1127 Some(self.update_lambda_layer_version(existing, new_def)?)
1128 }
1129 "AWS::Lambda::Url" => Some(self.update_lambda_url(existing, new_def)?),
1130 "AWS::Lambda::Alias" => Some(self.update_lambda_alias(existing, new_def)?),
1131 "AWS::Lambda::Version" => Some(self.update_lambda_version(existing, new_def)?),
1132 "AWS::ApiGateway::RestApi" => Some(self.update_apigw_rest_api(existing, new_def)?),
1133 "AWS::ApiGateway::Resource" => Some(self.update_apigw_resource(existing, new_def)?),
1134 "AWS::ApiGateway::Method" => Some(self.update_apigw_method(existing, new_def)?),
1135 "AWS::ApiGateway::Deployment" => Some(self.update_apigw_deployment(existing, new_def)?),
1136 "AWS::ApiGateway::Stage" => Some(self.update_apigw_stage(existing, new_def)?),
1137 "AWS::ApiGateway::Authorizer" => Some(self.update_apigw_authorizer(existing, new_def)?),
1138 "AWS::ApiGateway::RequestValidator" => {
1139 Some(self.update_apigw_request_validator(existing, new_def)?)
1140 }
1141 "AWS::ApiGateway::Model" => Some(self.update_apigw_model(existing, new_def)?),
1142 "AWS::ApiGateway::GatewayResponse" => {
1143 Some(self.update_apigw_gateway_response(existing, new_def)?)
1144 }
1145 "AWS::ApiGateway::UsagePlan" => Some(self.update_apigw_usage_plan(existing, new_def)?),
1146 "AWS::ApiGateway::ApiKey" => Some(self.update_apigw_api_key(existing, new_def)?),
1147 "AWS::ApiGateway::UsagePlanKey" => {
1148 Some(self.update_apigw_usage_plan_key(existing, new_def)?)
1149 }
1150 "AWS::ApiGateway::DomainName" => {
1151 Some(self.update_apigw_domain_name(existing, new_def)?)
1152 }
1153 "AWS::ApiGateway::BasePathMapping" => {
1154 Some(self.update_apigw_base_path_mapping(existing, new_def)?)
1155 }
1156 "AWS::ApiGatewayV2::Api" => Some(self.update_apigwv2_api(existing, new_def)?),
1157 "AWS::ApiGatewayV2::Route" => Some(self.update_apigwv2_route(existing, new_def)?),
1158 "AWS::ApiGatewayV2::Integration" => {
1159 Some(self.update_apigwv2_integration(existing, new_def)?)
1160 }
1161 "AWS::ApiGatewayV2::IntegrationResponse" => {
1162 Some(self.update_apigwv2_integration_response(existing, new_def)?)
1163 }
1164 "AWS::ApiGatewayV2::RouteResponse" => {
1165 Some(self.update_apigwv2_route_response(existing, new_def)?)
1166 }
1167 "AWS::ApiGatewayV2::Stage" => Some(self.update_apigwv2_stage(existing, new_def)?),
1168 "AWS::ApiGatewayV2::Deployment" => {
1169 Some(self.update_apigwv2_deployment(existing, new_def)?)
1170 }
1171 "AWS::ApiGatewayV2::Authorizer" => {
1172 Some(self.update_apigwv2_authorizer(existing, new_def)?)
1173 }
1174 "AWS::ApiGatewayV2::DomainName" => {
1175 Some(self.update_apigwv2_domain_name(existing, new_def)?)
1176 }
1177 "AWS::ApiGatewayV2::ApiMapping" => {
1178 Some(self.update_apigwv2_api_mapping(existing, new_def)?)
1179 }
1180 "AWS::ApiGatewayV2::VpcLink" => Some(self.update_apigwv2_vpc_link(existing, new_def)?),
1181 "AWS::ApiGatewayV2::Model" => Some(self.update_apigwv2_model(existing, new_def)?),
1182 "AWS::ECS::Cluster" => Some(self.update_ecs_cluster(existing, new_def)?),
1183 "AWS::ECS::Service" => Some(self.update_ecs_service(existing, new_def)?),
1184 "AWS::ECS::TaskDefinition" => Some(self.update_ecs_task_definition(existing, new_def)?),
1185 "AWS::ECS::CapacityProvider" => {
1186 Some(self.update_ecs_capacity_provider(existing, new_def)?)
1187 }
1188 "AWS::ECR::Repository" => Some(self.update_ecr_repository(existing, new_def)?),
1189 "AWS::ECR::RepositoryPolicy" => {
1190 Some(self.update_ecr_repository_policy(existing, new_def)?)
1191 }
1192 "AWS::ECR::LifecyclePolicy" => {
1193 Some(self.update_ecr_lifecycle_policy(existing, new_def)?)
1194 }
1195 "AWS::ECR::RegistryPolicy" => Some(self.update_ecr_registry_policy(existing, new_def)?),
1196 "AWS::ECR::ReplicationConfiguration" => {
1197 Some(self.update_ecr_replication_configuration(existing, new_def)?)
1198 }
1199 "AWS::ECR::RegistryScanningConfiguration" => {
1200 Some(self.update_ecr_registry_scanning_configuration(existing, new_def)?)
1201 }
1202 "AWS::ECR::PullThroughCacheRule" => {
1203 Some(self.update_ecr_pull_through_cache_rule(existing, new_def)?)
1204 }
1205 "AWS::KMS::Key" => Some(self.update_kms_key(existing, new_def)?),
1206 "AWS::KMS::ReplicaKey" => Some(self.update_kms_replica_key(existing, new_def)?),
1207 "AWS::KMS::Alias" => Some(self.update_kms_alias(existing, new_def)?),
1208 "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1209 Some(self.update_elbv2_load_balancer(existing, new_def)?)
1210 }
1211 "AWS::ElasticLoadBalancingV2::TargetGroup" => {
1212 Some(self.update_elbv2_target_group(existing, new_def)?)
1213 }
1214 "AWS::ElasticLoadBalancingV2::Listener" => {
1215 Some(self.update_elbv2_listener(existing, new_def)?)
1216 }
1217 "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1218 Some(self.update_elbv2_listener_rule(existing, new_def)?)
1219 }
1220 "AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
1221 Some(self.update_elbv2_listener_certificate(existing, new_def)?)
1222 }
1223 "AWS::ElasticLoadBalancingV2::TrustStore" => {
1224 Some(self.update_elbv2_trust_store(existing, new_def)?)
1225 }
1226 "AWS::CloudWatch::Alarm" => Some(self.update_cloudwatch_alarm(existing, new_def)?),
1227 "AWS::CloudWatch::Dashboard" => {
1228 Some(self.update_cloudwatch_dashboard(existing, new_def)?)
1229 }
1230 _ => None,
1231 };
1232
1233 Ok(result.map(|res| StackResource {
1234 logical_id: existing.logical_id.clone(),
1235 physical_id: res.physical_id,
1236 resource_type: existing.resource_type.clone(),
1237 status: "UPDATE_COMPLETE".to_string(),
1238 service_token: existing.service_token.clone(),
1239 attributes: res.attributes,
1240 }))
1241 }
1242
1243 pub fn get_att(&self, resource: &StackResource, attribute: &str) -> Option<String> {
1254 if let Some(v) = resource.attributes.get(attribute) {
1257 return Some(v.clone());
1258 }
1259 match resource.resource_type.as_str() {
1262 "AWS::S3::Bucket" => self.get_att_s3_bucket(&resource.physical_id, attribute),
1263 "AWS::Lambda::Function" => {
1264 self.get_att_lambda_function(&resource.physical_id, attribute)
1265 }
1266 "AWS::IAM::Role" => self.get_att_iam_role(&resource.physical_id, attribute),
1267 "AWS::SQS::Queue" => self.get_att_sqs_queue(&resource.physical_id, attribute),
1268 "AWS::SNS::Topic" => self.get_att_sns_topic(&resource.physical_id, attribute),
1269 "AWS::DynamoDB::Table" => self.get_att_dynamodb_table(&resource.physical_id, attribute),
1270 "AWS::KMS::Key" => self.get_att_kms_key(&resource.physical_id, attribute),
1271 "AWS::SecretsManager::Secret" => {
1272 self.get_att_secrets_manager_secret(&resource.physical_id, attribute)
1273 }
1274 "AWS::CloudFront::Distribution" => {
1275 self.get_att_cf_distribution(&resource.physical_id, attribute)
1276 }
1277 "AWS::ECS::Cluster" => self.get_att_ecs_cluster(&resource.physical_id, attribute),
1278 "AWS::ECS::Service" => self.get_att_ecs_service(&resource.physical_id, attribute),
1279 "AWS::ECS::CapacityProvider" => {
1280 self.get_att_ecs_capacity_provider(&resource.physical_id, attribute)
1281 }
1282 "AWS::ECR::Repository" => self.get_att_ecr_repository(&resource.physical_id, attribute),
1283 "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1284 self.get_att_elbv2_load_balancer(&resource.physical_id, attribute)
1285 }
1286 "AWS::ElasticLoadBalancingV2::TargetGroup" => {
1287 self.get_att_elbv2_target_group(&resource.physical_id, attribute)
1288 }
1289 "AWS::ElasticLoadBalancingV2::Listener" => {
1290 self.get_att_elbv2_listener(&resource.physical_id, attribute)
1291 }
1292 "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1293 self.get_att_elbv2_listener_rule(&resource.physical_id, attribute)
1294 }
1295 "AWS::ElasticLoadBalancingV2::TrustStore" => {
1296 self.get_att_elbv2_trust_store(&resource.physical_id, attribute)
1297 }
1298 "AWS::WAFv2::WebACL" => self.get_att_wafv2_web_acl(&resource.physical_id, attribute),
1299 "AWS::WAFv2::IPSet" => self.get_att_wafv2_ip_set(&resource.physical_id, attribute),
1300 "AWS::WAFv2::RegexPatternSet" => {
1301 self.get_att_wafv2_regex_pattern_set(&resource.physical_id, attribute)
1302 }
1303 "AWS::WAFv2::RuleGroup" => {
1304 self.get_att_wafv2_rule_group(&resource.physical_id, attribute)
1305 }
1306 "AWS::SES::ConfigurationSet" => {
1307 self.get_att_ses_configuration_set(&resource.physical_id, attribute)
1308 }
1309 "AWS::SES::EmailIdentity" => {
1310 self.get_att_ses_email_identity(&resource.physical_id, attribute)
1311 }
1312 "AWS::SES::Template" => self.get_att_ses_template(&resource.physical_id, attribute),
1313 "AWS::SES::ContactList" => {
1314 self.get_att_ses_contact_list(&resource.physical_id, attribute)
1315 }
1316 "AWS::SES::DedicatedIpPool" => {
1317 self.get_att_ses_dedicated_ip_pool(&resource.physical_id, attribute)
1318 }
1319 "AWS::SES::ReceiptRuleSet" => {
1320 self.get_att_ses_receipt_rule_set(&resource.physical_id, attribute)
1321 }
1322 "AWS::Athena::DataCatalog" => {
1323 self.get_att_athena_data_catalog(&resource.physical_id, attribute)
1324 }
1325 "AWS::Athena::NamedQuery" => {
1326 self.get_att_athena_named_query(&resource.physical_id, attribute)
1327 }
1328 "AWS::Athena::WorkGroup" => {
1329 self.get_att_athena_work_group(&resource.physical_id, attribute)
1330 }
1331 "AWS::Athena::PreparedStatement" => {
1332 self.get_att_athena_prepared_statement(&resource.physical_id, attribute)
1333 }
1334 "AWS::CloudFormation::Stack" => {
1335 self.get_att_cloudformation_stack(&resource.physical_id, attribute)
1336 }
1337 _ => None,
1338 }
1339 }
1340
1341 fn get_att_ses_configuration_set(&self, physical_id: &str, attribute: &str) -> Option<String> {
1342 let mut accounts = self.ses_state.write();
1343 let state = accounts.get_or_create(&self.account_id);
1344 let cs = state.configuration_sets.get(physical_id)?;
1345 match attribute {
1346 "Name" => Some(cs.name.clone()),
1347 _ => None,
1348 }
1349 }
1350
1351 fn get_att_ses_email_identity(&self, physical_id: &str, attribute: &str) -> Option<String> {
1352 let mut accounts = self.ses_state.write();
1353 let state = accounts.get_or_create(&self.account_id);
1354 let id = state.identities.get(physical_id)?;
1355 match attribute {
1356 "IdentityName" => Some(id.identity_name.clone()),
1357 _ => None,
1358 }
1359 }
1360
1361 fn get_att_ses_template(&self, physical_id: &str, attribute: &str) -> Option<String> {
1362 let mut accounts = self.ses_state.write();
1363 let state = accounts.get_or_create(&self.account_id);
1364 let tpl = state.templates.get(physical_id)?;
1365 match attribute {
1366 "TemplateName" => Some(tpl.template_name.clone()),
1367 _ => None,
1368 }
1369 }
1370
1371 fn get_att_ses_contact_list(&self, physical_id: &str, attribute: &str) -> Option<String> {
1372 let mut accounts = self.ses_state.write();
1373 let state = accounts.get_or_create(&self.account_id);
1374 let cl = state.contact_lists.get(physical_id)?;
1375 match attribute {
1376 "ContactListName" => Some(cl.contact_list_name.clone()),
1377 _ => None,
1378 }
1379 }
1380
1381 fn get_att_ses_dedicated_ip_pool(&self, physical_id: &str, attribute: &str) -> Option<String> {
1382 let mut accounts = self.ses_state.write();
1383 let state = accounts.get_or_create(&self.account_id);
1384 let pool = state.dedicated_ip_pools.get(physical_id)?;
1385 match attribute {
1386 "PoolName" => Some(pool.pool_name.clone()),
1387 _ => None,
1388 }
1389 }
1390
1391 fn get_att_ses_receipt_rule_set(&self, physical_id: &str, attribute: &str) -> Option<String> {
1392 let mut accounts = self.ses_state.write();
1393 let state = accounts.get_or_create(&self.account_id);
1394 let rs = state.receipt_rule_sets.get(physical_id)?;
1395 match attribute {
1396 "RuleSetName" => Some(rs.name.clone()),
1397 _ => None,
1398 }
1399 }
1400
1401 fn get_att_wafv2_web_acl(&self, physical_id: &str, attribute: &str) -> Option<String> {
1402 let mut accounts = self.wafv2_state.write();
1403 let state = accounts
1404 .accounts
1405 .entry(self.account_id.clone())
1406 .or_default();
1407 let acl = state.web_acls.values().find(|a| a.arn == physical_id)?;
1408 match attribute {
1409 "Arn" => Some(acl.arn.clone()),
1410 "Id" => Some(acl.id.clone()),
1411 "Name" => Some(acl.name.clone()),
1412 "LabelNamespace" => Some(acl.label_namespace.clone()),
1413 "Capacity" => Some(acl.capacity.to_string()),
1414 _ => None,
1415 }
1416 }
1417
1418 fn get_att_wafv2_ip_set(&self, physical_id: &str, attribute: &str) -> Option<String> {
1419 let mut accounts = self.wafv2_state.write();
1420 let state = accounts
1421 .accounts
1422 .entry(self.account_id.clone())
1423 .or_default();
1424 let ip_set = state.ip_sets.values().find(|i| i.arn == physical_id)?;
1425 match attribute {
1426 "Arn" => Some(ip_set.arn.clone()),
1427 "Id" => Some(ip_set.id.clone()),
1428 "Name" => Some(ip_set.name.clone()),
1429 _ => None,
1430 }
1431 }
1432
1433 fn get_att_wafv2_regex_pattern_set(
1434 &self,
1435 physical_id: &str,
1436 attribute: &str,
1437 ) -> Option<String> {
1438 let mut accounts = self.wafv2_state.write();
1439 let state = accounts
1440 .accounts
1441 .entry(self.account_id.clone())
1442 .or_default();
1443 let set = state
1444 .regex_pattern_sets
1445 .values()
1446 .find(|r| r.arn == physical_id)?;
1447 match attribute {
1448 "Arn" => Some(set.arn.clone()),
1449 "Id" => Some(set.id.clone()),
1450 "Name" => Some(set.name.clone()),
1451 _ => None,
1452 }
1453 }
1454
1455 fn get_att_wafv2_rule_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
1456 let mut accounts = self.wafv2_state.write();
1457 let state = accounts
1458 .accounts
1459 .entry(self.account_id.clone())
1460 .or_default();
1461 let rg = state.rule_groups.values().find(|r| r.arn == physical_id)?;
1462 match attribute {
1463 "Arn" => Some(rg.arn.clone()),
1464 "Id" => Some(rg.id.clone()),
1465 "Name" => Some(rg.name.clone()),
1466 _ => None,
1467 }
1468 }
1469
1470 fn get_att_s3_bucket(&self, physical_id: &str, attribute: &str) -> Option<String> {
1471 let mut accounts = self.s3_state.write();
1472 let state = accounts.get_or_create(&self.account_id);
1473 let bucket = state.buckets.get(physical_id)?;
1474 match attribute {
1475 "Arn" => Some(format!("arn:aws:s3:::{}", bucket.name)),
1476 "DomainName" => Some(format!("{}.s3.amazonaws.com", bucket.name)),
1477 "RegionalDomainName" => {
1478 Some(format!("{}.s3.{}.amazonaws.com", bucket.name, self.region))
1479 }
1480 "DualStackDomainName" => Some(format!(
1481 "{}.s3.dualstack.{}.amazonaws.com",
1482 bucket.name, self.region
1483 )),
1484 "WebsiteURL" => Some(format!(
1485 "http://{}.s3-website-{}.amazonaws.com",
1486 bucket.name, self.region
1487 )),
1488 _ => None,
1489 }
1490 }
1491
1492 fn get_att_lambda_function(&self, physical_id: &str, attribute: &str) -> Option<String> {
1493 let function_name = parse_lambda_function_name(physical_id);
1494 let mut accounts = self.lambda_state.write();
1495 let state = accounts.get_or_create(&self.account_id);
1496 match attribute {
1497 "Arn" => state
1498 .functions
1499 .get(&function_name)
1500 .map(|f| f.function_arn.clone()),
1501 "FunctionUrl" => state
1502 .function_url_configs
1503 .get(&function_name)
1504 .map(|u| u.function_url.clone()),
1505 "Version" => state
1506 .functions
1507 .get(&function_name)
1508 .map(|f| f.version.clone()),
1509 _ => None,
1510 }
1511 }
1512
1513 fn get_att_iam_role(&self, physical_id: &str, attribute: &str) -> Option<String> {
1514 let mut accounts = self.iam_state.write();
1515 let state = accounts.get_or_create(&self.account_id);
1516 let role = state
1519 .roles
1520 .values()
1521 .find(|r| r.arn == physical_id || r.role_name == physical_id)?;
1522 match attribute {
1523 "Arn" => Some(role.arn.clone()),
1524 "RoleId" => Some(role.role_id.clone()),
1525 _ => None,
1526 }
1527 }
1528
1529 fn get_att_sqs_queue(&self, physical_id: &str, attribute: &str) -> Option<String> {
1530 let mut accounts = self.sqs_state.write();
1531 let state = accounts.get_or_create(&self.account_id);
1532 let queue = state.queues.get(physical_id)?;
1533 match attribute {
1534 "Arn" => Some(queue.arn.clone()),
1535 "QueueName" => Some(queue.queue_name.clone()),
1536 "QueueUrl" => Some(queue.queue_url.clone()),
1537 _ => None,
1538 }
1539 }
1540
1541 fn get_att_sns_topic(&self, physical_id: &str, attribute: &str) -> Option<String> {
1542 let mut accounts = self.sns_state.write();
1543 let state = accounts.get_or_create(&self.account_id);
1544 let topic = state.topics.get(physical_id)?;
1545 match attribute {
1546 "TopicArn" => Some(topic.topic_arn.clone()),
1547 "TopicName" => Some(topic.name.clone()),
1548 _ => None,
1549 }
1550 }
1551
1552 fn get_att_dynamodb_table(&self, physical_id: &str, attribute: &str) -> Option<String> {
1553 let mut accounts = self.dynamodb_state.write();
1554 let state = accounts.get_or_create(&self.account_id);
1555 let table = state.tables.get(physical_id)?;
1556 match attribute {
1557 "Arn" => Some(table.arn.clone()),
1558 "StreamArn" => table.stream_arn.clone(),
1559 _ => None,
1560 }
1561 }
1562
1563 fn get_att_kms_key(&self, physical_id: &str, attribute: &str) -> Option<String> {
1564 let mut accounts = self.kms_state.write();
1565 let state = accounts.get_or_create(&self.account_id);
1566 let key = state.keys.get(physical_id)?;
1567 match attribute {
1568 "Arn" => Some(key.arn.clone()),
1569 "KeyId" => Some(key.key_id.clone()),
1570 _ => None,
1571 }
1572 }
1573
1574 fn get_att_secrets_manager_secret(&self, physical_id: &str, attribute: &str) -> Option<String> {
1575 let mut accounts = self.secretsmanager_state.write();
1576 let state = accounts.get_or_create(&self.account_id);
1577 let secret = state.secrets.get(physical_id)?;
1578 match attribute {
1579 "Arn" | "Id" => Some(secret.arn.clone()),
1582 _ => None,
1583 }
1584 }
1585
1586 fn get_att_cf_distribution(&self, physical_id: &str, attribute: &str) -> Option<String> {
1587 let accounts = self.cloudfront_state.read();
1590 let state = accounts.get("000000000000")?;
1591 let dist = state.distributions.get(physical_id)?;
1592 match attribute {
1593 "DomainName" => Some(dist.domain_name.clone()),
1594 "Id" => Some(dist.id.clone()),
1595 _ => None,
1596 }
1597 }
1598
1599 pub fn delete_resource(&self, resource: &StackResource) -> Result<(), String> {
1601 match resource.resource_type.as_str() {
1602 "AWS::SQS::Queue" => self.delete_sqs_queue(&resource.physical_id),
1603 "AWS::SNS::Topic" => self.delete_sns_topic(&resource.physical_id),
1604 "AWS::SNS::Subscription" => self.delete_sns_subscription(&resource.physical_id),
1605 "AWS::SSM::Parameter" => self.delete_ssm_parameter(&resource.physical_id),
1606 "AWS::IAM::Role" => self.delete_iam_role(&resource.physical_id),
1607 "AWS::IAM::Policy" => self.delete_iam_policy(&resource.physical_id),
1608 "AWS::IAM::User" => self.delete_iam_user(&resource.physical_id),
1609 "AWS::IAM::Group" => self.delete_iam_group(&resource.physical_id),
1610 "AWS::IAM::ManagedPolicy" => self.delete_iam_managed_policy(&resource.physical_id),
1611 "AWS::IAM::UserToGroupAddition" => {
1612 self.delete_iam_user_to_group_addition(&resource.physical_id)
1613 }
1614 "AWS::IAM::AccessKey" => self.delete_iam_access_key(&resource.physical_id),
1615 "AWS::IAM::InstanceProfile" => self.delete_iam_instance_profile(&resource.physical_id),
1616 "AWS::IAM::OIDCProvider" => self.delete_iam_oidc_provider(&resource.physical_id),
1617 "AWS::IAM::SAMLProvider" => self.delete_iam_saml_provider(&resource.physical_id),
1618 "AWS::IAM::ServiceLinkedRole" => {
1619 self.delete_iam_service_linked_role(&resource.physical_id)
1620 }
1621 "AWS::IAM::VirtualMFADevice" => {
1622 self.delete_iam_virtual_mfa_device(&resource.physical_id)
1623 }
1624 "AWS::S3::Bucket" => self.delete_s3_bucket(&resource.physical_id),
1625 "AWS::Events::Rule" => self.delete_eventbridge_rule(&resource.physical_id),
1626 "AWS::Events::Connection" => self.delete_eventbridge_connection(&resource.physical_id),
1627 "AWS::Events::EventBus" => self.delete_eventbridge_event_bus(&resource.physical_id),
1628 "AWS::Events::EventBusPolicy" => {
1629 self.delete_eventbridge_event_bus_policy(&resource.physical_id)
1630 }
1631 "AWS::Events::Endpoint" => self.delete_eventbridge_endpoint(&resource.physical_id),
1632 "AWS::Events::ApiDestination" => {
1633 self.delete_eventbridge_api_destination(&resource.physical_id)
1634 }
1635 "AWS::Events::Archive" => self.delete_eventbridge_archive(&resource.physical_id),
1636 "AWS::DynamoDB::Table" => self.delete_dynamodb_table(&resource.physical_id),
1637 "AWS::Logs::LogGroup" => self.delete_log_group(&resource.physical_id),
1638 "AWS::Logs::LogStream" => self.delete_log_stream(&resource.physical_id),
1639 "AWS::Logs::MetricFilter" => self.delete_metric_filter(&resource.physical_id),
1640 "AWS::Logs::SubscriptionFilter" => {
1641 self.delete_subscription_filter(&resource.physical_id)
1642 }
1643 "AWS::Logs::Destination" => self.delete_logs_destination(&resource.physical_id),
1644 "AWS::Logs::ResourcePolicy" => self.delete_logs_resource_policy(&resource.physical_id),
1645 "AWS::Logs::QueryDefinition" => {
1646 self.delete_logs_query_definition(&resource.physical_id)
1647 }
1648 "AWS::Logs::Delivery" => self.delete_logs_delivery(&resource.physical_id),
1649 "AWS::Logs::DeliveryDestination" => {
1650 self.delete_logs_delivery_destination(&resource.physical_id)
1651 }
1652 "AWS::Logs::DeliverySource" => self.delete_logs_delivery_source(&resource.physical_id),
1653 "AWS::Lambda::Function" => self.delete_lambda_function(&resource.physical_id),
1654 "AWS::Lambda::Permission" => self.delete_lambda_permission(&resource.physical_id),
1655 "AWS::Lambda::EventSourceMapping" => {
1656 self.delete_lambda_event_source_mapping(&resource.physical_id)
1657 }
1658 "AWS::Lambda::LayerVersion" => self.delete_lambda_layer_version(&resource.physical_id),
1659 "AWS::Lambda::Url" => self.delete_lambda_url(&resource.physical_id),
1660 "AWS::Lambda::Alias" => self.delete_lambda_alias(&resource.physical_id),
1661 "AWS::Lambda::Version" => self.delete_lambda_version(&resource.physical_id),
1662 "AWS::SecretsManager::Secret" => {
1663 self.delete_secrets_manager_secret(&resource.physical_id)
1664 }
1665 "AWS::Kinesis::Stream" => self.delete_kinesis_stream(&resource.physical_id),
1666 "AWS::Kinesis::StreamConsumer" => {
1667 self.delete_kinesis_stream_consumer(&resource.physical_id)
1668 }
1669 "AWS::KMS::Key" => self.delete_kms_key(&resource.physical_id),
1670 "AWS::KMS::ReplicaKey" => self.delete_kms_replica_key(&resource.physical_id),
1671 "AWS::KMS::Alias" => self.delete_kms_alias(&resource.physical_id),
1672 "AWS::ECR::Repository" => self.delete_ecr_repository(&resource.physical_id),
1673 "AWS::ECR::RepositoryPolicy" => {
1674 self.delete_ecr_repository_policy(&resource.physical_id)
1675 }
1676 "AWS::ECR::LifecyclePolicy" => self.delete_ecr_lifecycle_policy(&resource.physical_id),
1677 "AWS::ECR::RegistryPolicy" => self.delete_ecr_registry_policy(),
1678 "AWS::ECR::ReplicationConfiguration" => self.delete_ecr_replication_configuration(),
1679 "AWS::ECR::RegistryScanningConfiguration" => {
1680 self.delete_ecr_registry_scanning_configuration()
1681 }
1682 "AWS::ECR::PullThroughCacheRule" => {
1683 self.delete_ecr_pull_through_cache_rule(&resource.physical_id)
1684 }
1685 "AWS::CloudWatch::Alarm" => self.delete_cloudwatch_alarm(&resource.physical_id),
1686 "AWS::CloudWatch::Dashboard" => self.delete_cloudwatch_dashboard(&resource.physical_id),
1687 "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1688 self.delete_elbv2_load_balancer(&resource.physical_id)
1689 }
1690 "AWS::ElasticLoadBalancingV2::TargetGroup" => {
1691 self.delete_elbv2_target_group(&resource.physical_id)
1692 }
1693 "AWS::ElasticLoadBalancingV2::Listener" => {
1694 self.delete_elbv2_listener(&resource.physical_id)
1695 }
1696 "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1697 self.delete_elbv2_listener_rule(&resource.physical_id)
1698 }
1699 "AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
1700 self.delete_elbv2_listener_certificate(&resource.physical_id)
1701 }
1702 "AWS::ElasticLoadBalancingV2::TrustStore" => {
1703 self.delete_elbv2_trust_store(&resource.physical_id)
1704 }
1705 "AWS::Organizations::Organization" => self.delete_organization(&resource.physical_id),
1706 "AWS::Organizations::OrganizationalUnit" => {
1707 self.delete_organization_unit(&resource.physical_id)
1708 }
1709 "AWS::Organizations::Account" => {
1710 self.delete_organization_account(&resource.physical_id)
1711 }
1712 "AWS::Organizations::Policy" => self.delete_organization_policy(&resource.physical_id),
1713 "AWS::Organizations::ResourcePolicy" => {
1714 self.delete_organization_resource_policy(&resource.physical_id)
1715 }
1716 "AWS::Cognito::UserPool" => self.delete_cognito_user_pool(&resource.physical_id),
1717 "AWS::Cognito::UserPoolClient" => {
1718 self.delete_cognito_user_pool_client(&resource.physical_id)
1719 }
1720 "AWS::Cognito::UserPoolDomain" => {
1721 self.delete_cognito_user_pool_domain(&resource.physical_id)
1722 }
1723 "AWS::Cognito::IdentityPool" => {
1724 self.delete_cognito_identity_pool(&resource.physical_id)
1725 }
1726 "AWS::Cognito::IdentityPoolRoleAttachment" => {
1727 self.delete_cognito_identity_pool_role_attachment(&resource.physical_id)
1728 }
1729 "AWS::RDS::DBSubnetGroup" => self.delete_rds_subnet_group(&resource.physical_id),
1730 "AWS::RDS::DBParameterGroup" => self.delete_rds_parameter_group(&resource.physical_id),
1731 "AWS::RDS::DBClusterParameterGroup" => {
1732 self.delete_rds_cluster_parameter_group(&resource.physical_id)
1733 }
1734 "AWS::RDS::OptionGroup" => self.delete_rds_option_group(&resource.physical_id),
1735 "AWS::RDS::EventSubscription" => {
1736 self.delete_rds_event_subscription(&resource.physical_id)
1737 }
1738 "AWS::RDS::DBSecurityGroup" => self.delete_rds_security_group(&resource.physical_id),
1739 "AWS::RDS::DBProxy" => self.delete_rds_db_proxy(&resource.physical_id),
1740 "AWS::RDS::DBInstance" => self.delete_rds_db_instance(&resource.physical_id),
1741 "AWS::RDS::DBCluster" => self.delete_rds_db_cluster(&resource.physical_id),
1742 "AWS::ECS::Cluster" => self.delete_ecs_cluster(&resource.physical_id),
1743 "AWS::ECS::TaskDefinition" => self.delete_ecs_task_definition(&resource.physical_id),
1744 "AWS::ECS::Service" => self.delete_ecs_service(&resource.physical_id),
1745 "AWS::ECS::CapacityProvider" => {
1746 self.delete_ecs_capacity_provider(&resource.physical_id)
1747 }
1748 "AWS::CertificateManager::Certificate" => {
1749 self.delete_acm_certificate(&resource.physical_id)
1750 }
1751 "AWS::CertificateManager::Account" => self.delete_acm_account(),
1752 "AWS::ElastiCache::ParameterGroup" => {
1753 self.delete_ec_parameter_group(&resource.physical_id)
1754 }
1755 "AWS::ElastiCache::SubnetGroup" => self.delete_ec_subnet_group(&resource.physical_id),
1756 "AWS::ElastiCache::SecurityGroup" => {
1757 self.delete_ec_security_group(&resource.physical_id)
1758 }
1759 "AWS::ElastiCache::User" => self.delete_ec_user(&resource.physical_id),
1760 "AWS::ElastiCache::UserGroup" => self.delete_ec_user_group(&resource.physical_id),
1761 "AWS::ElastiCache::CacheCluster" => self.delete_ec_cache_cluster(&resource.physical_id),
1762 "AWS::ElastiCache::ReplicationGroup" => {
1763 self.delete_ec_replication_group(&resource.physical_id)
1764 }
1765 "AWS::Route53::HostedZone" => self.delete_route53_hosted_zone(&resource.physical_id),
1766 "AWS::Route53::RecordSet" => {
1767 self.delete_route53_record_set(&resource.physical_id, &resource.attributes)
1768 }
1769 "AWS::Route53::HealthCheck" => self.delete_route53_health_check(&resource.physical_id),
1770 "AWS::Route53::DNSSEC" => self.delete_route53_dnssec(&resource.physical_id),
1771 "AWS::Route53::KeySigningKey" => {
1772 self.delete_route53_key_signing_key(&resource.physical_id)
1773 }
1774 "AWS::CloudFront::CloudFrontOriginAccessIdentity" => {
1775 self.delete_cf_origin_access_identity(&resource.physical_id)
1776 }
1777 "AWS::CloudFront::Distribution" => self.delete_cf_distribution(&resource.physical_id),
1778 "AWS::CloudFront::OriginAccessControl" => {
1779 self.delete_cf_origin_access_control(&resource.physical_id)
1780 }
1781 "AWS::CloudFront::PublicKey" => self.delete_cf_public_key(&resource.physical_id),
1782 "AWS::CloudFront::KeyGroup" => self.delete_cf_key_group(&resource.physical_id),
1783 "AWS::CloudFront::Function" => self.delete_cf_function(&resource.physical_id),
1784 "AWS::CloudFront::CachePolicy" => self.delete_cf_cache_policy(&resource.physical_id),
1785 "AWS::CloudFront::OriginRequestPolicy" => {
1786 self.delete_cf_origin_request_policy(&resource.physical_id)
1787 }
1788 "AWS::CloudFront::ResponseHeadersPolicy" => {
1789 self.delete_cf_response_headers_policy(&resource.physical_id)
1790 }
1791 "AWS::StepFunctions::StateMachine" => {
1792 self.delete_sfn_state_machine(&resource.physical_id)
1793 }
1794 "AWS::StepFunctions::Activity" => self.delete_sfn_activity(&resource.physical_id),
1795 "AWS::StepFunctions::StateMachineVersion" => {
1796 self.delete_sfn_version(&resource.physical_id)
1797 }
1798 "AWS::StepFunctions::StateMachineAlias" => self.delete_sfn_alias(&resource.physical_id),
1799 "AWS::WAFv2::WebACL" => self.delete_wafv2_web_acl(&resource.physical_id),
1800 "AWS::WAFv2::IPSet" => self.delete_wafv2_ip_set(&resource.physical_id),
1801 "AWS::WAFv2::RegexPatternSet" => {
1802 self.delete_wafv2_regex_pattern_set(&resource.physical_id)
1803 }
1804 "AWS::WAFv2::RuleGroup" => self.delete_wafv2_rule_group(&resource.physical_id),
1805 "AWS::WAFv2::LoggingConfiguration" => {
1806 self.delete_wafv2_logging_configuration(&resource.physical_id)
1807 }
1808 "AWS::WAFv2::WebACLAssociation" => {
1809 self.delete_wafv2_web_acl_association(&resource.physical_id)
1810 }
1811 "AWS::ApiGateway::RestApi" => self.delete_apigw_rest_api(&resource.physical_id),
1812 "AWS::ApiGateway::Resource" => {
1813 self.delete_apigw_resource(&resource.physical_id, &resource.attributes)
1814 }
1815 "AWS::ApiGateway::Method" => self.delete_apigw_method(&resource.physical_id),
1816 "AWS::ApiGateway::Deployment" => {
1817 self.delete_apigw_deployment(&resource.physical_id, &resource.attributes)
1818 }
1819 "AWS::ApiGateway::Stage" => {
1820 self.delete_apigw_stage(&resource.physical_id, &resource.attributes)
1821 }
1822 "AWS::ApiGateway::Authorizer" => {
1823 self.delete_apigw_authorizer(&resource.physical_id, &resource.attributes)
1824 }
1825 "AWS::ApiGateway::RequestValidator" => {
1826 self.delete_apigw_request_validator(&resource.physical_id, &resource.attributes)
1827 }
1828 "AWS::ApiGateway::Model" => {
1829 self.delete_apigw_model(&resource.physical_id, &resource.attributes)
1830 }
1831 "AWS::ApiGateway::GatewayResponse" => {
1832 self.delete_apigw_gateway_response(&resource.physical_id, &resource.attributes)
1833 }
1834 "AWS::ApiGateway::UsagePlan" => self.delete_apigw_usage_plan(&resource.physical_id),
1835 "AWS::ApiGateway::ApiKey" => self.delete_apigw_api_key(&resource.physical_id),
1836 "AWS::ApiGateway::UsagePlanKey" => {
1837 self.delete_apigw_usage_plan_key(&resource.physical_id, &resource.attributes)
1838 }
1839 "AWS::ApiGateway::DomainName" => self.delete_apigw_domain_name(&resource.physical_id),
1840 "AWS::ApiGateway::BasePathMapping" => {
1841 self.delete_apigw_base_path_mapping(&resource.physical_id, &resource.attributes)
1842 }
1843 "AWS::ApiGatewayV2::Api" => self.delete_apigwv2_api(&resource.physical_id),
1844 "AWS::ApiGatewayV2::Route" => {
1845 self.delete_apigwv2_route(&resource.physical_id, &resource.attributes)
1846 }
1847 "AWS::ApiGatewayV2::Integration" => {
1848 self.delete_apigwv2_integration(&resource.physical_id, &resource.attributes)
1849 }
1850 "AWS::ApiGatewayV2::IntegrationResponse" => self
1851 .delete_apigwv2_integration_response(&resource.physical_id, &resource.attributes),
1852 "AWS::ApiGatewayV2::RouteResponse" => {
1853 self.delete_apigwv2_route_response(&resource.physical_id, &resource.attributes)
1854 }
1855 "AWS::ApiGatewayV2::Stage" => {
1856 self.delete_apigwv2_stage(&resource.physical_id, &resource.attributes)
1857 }
1858 "AWS::ApiGatewayV2::Deployment" => {
1859 self.delete_apigwv2_deployment(&resource.physical_id, &resource.attributes)
1860 }
1861 "AWS::ApiGatewayV2::Authorizer" => {
1862 self.delete_apigwv2_authorizer(&resource.physical_id, &resource.attributes)
1863 }
1864 "AWS::ApiGatewayV2::DomainName" => {
1865 self.delete_apigwv2_domain_name(&resource.physical_id)
1866 }
1867 "AWS::ApiGatewayV2::ApiMapping" => {
1868 self.delete_apigwv2_api_mapping(&resource.physical_id, &resource.attributes)
1869 }
1870 "AWS::ApiGatewayV2::VpcLink" => self.delete_apigwv2_vpc_link(&resource.physical_id),
1871 "AWS::ApiGatewayV2::Model" => {
1872 self.delete_apigwv2_model(&resource.physical_id, &resource.attributes)
1873 }
1874 "AWS::SES::ConfigurationSet" => {
1875 self.delete_ses_configuration_set(&resource.physical_id)
1876 }
1877 "AWS::SES::ConfigurationSetEventDestination" => {
1878 self.delete_ses_event_destination(&resource.physical_id, &resource.attributes)
1879 }
1880 "AWS::SES::EmailIdentity" => self.delete_ses_email_identity(&resource.physical_id),
1881 "AWS::SES::Template" => self.delete_ses_template(&resource.physical_id),
1882 "AWS::SES::ContactList" => self.delete_ses_contact_list(&resource.physical_id),
1883 "AWS::SES::DedicatedIpPool" => self.delete_ses_dedicated_ip_pool(&resource.physical_id),
1884 "AWS::SES::ReceiptRule" => {
1885 self.delete_ses_receipt_rule(&resource.physical_id, &resource.attributes)
1886 }
1887 "AWS::SES::ReceiptRuleSet" => self.delete_ses_receipt_rule_set(&resource.physical_id),
1888 "AWS::SES::ReceiptFilter" => self.delete_ses_receipt_filter(&resource.physical_id),
1889 "AWS::SES::VdmAttributes" => Ok(()),
1890 "AWS::SecretsManager::RotationSchedule" => {
1891 self.delete_secrets_manager_rotation_schedule(&resource.physical_id)
1892 }
1893 "AWS::SecretsManager::ResourcePolicy" => {
1894 self.delete_secrets_manager_resource_policy(&resource.physical_id)
1895 }
1896 "AWS::SecretsManager::SecretTargetAttachment" => Ok(()),
1897 "AWS::ApplicationAutoScaling::ScalableTarget" => self
1898 .delete_application_autoscaling_scalable_target(
1899 &resource.physical_id,
1900 &resource.attributes,
1901 ),
1902 "AWS::ApplicationAutoScaling::ScalingPolicy" => self
1903 .delete_application_autoscaling_scaling_policy(
1904 &resource.physical_id,
1905 &resource.attributes,
1906 ),
1907 "AWS::Athena::DataCatalog" => self.delete_athena_data_catalog(&resource.physical_id),
1908 "AWS::Athena::NamedQuery" => self.delete_athena_named_query(&resource.physical_id),
1909 "AWS::Athena::WorkGroup" => self.delete_athena_work_group(&resource.physical_id),
1910 "AWS::Athena::PreparedStatement" => {
1911 self.delete_athena_prepared_statement(&resource.physical_id, &resource.attributes)
1912 }
1913 "AWS::KinesisFirehose::DeliveryStream" => {
1914 self.delete_firehose_delivery_stream(&resource.physical_id)
1915 }
1916 "AWS::Glue::Database" => self.delete_glue_database(&resource.physical_id),
1917 "AWS::CloudFormation::Stack" => self.delete_cloudformation_stack(&resource.physical_id),
1918 "AWS::Glue::Table" => self.delete_glue_table(&resource.physical_id),
1919 "AWS::Glue::Partition" => {
1920 self.delete_glue_partition(&resource.physical_id, &resource.attributes)
1921 }
1922 t if t.starts_with("Custom::") || t == "AWS::CloudFormation::CustomResource" => {
1923 self.delete_custom_resource(resource)
1924 }
1925 other => Err(format!("Unsupported resource type: {other}")),
1926 }
1927 }
1928
1929 fn create_sqs_queue(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
1932 let props = &resource.properties;
1933 let queue_name = props
1934 .get("QueueName")
1935 .and_then(|v| v.as_str())
1936 .unwrap_or(&resource.logical_id);
1937
1938 let mut __sqs_mas = self.sqs_state.write();
1939 let state = __sqs_mas.get_or_create(&self.account_id);
1940 let queue_url = format!("{}/{}/{}", state.endpoint, state.account_id, queue_name);
1941 let arn = format!(
1942 "arn:aws:sqs:{}:{}:{}",
1943 state.region, state.account_id, queue_name
1944 );
1945
1946 let is_fifo = queue_name.ends_with(".fifo");
1947 let mut attributes = std::collections::BTreeMap::new();
1948 if let Some(obj) = props.as_object() {
1949 for (k, v) in obj {
1950 if k != "QueueName" {
1951 if let Some(s) = v.as_str() {
1952 attributes.insert(k.clone(), s.to_string());
1953 } else if let Some(n) = v.as_i64() {
1954 attributes.insert(k.clone(), n.to_string());
1955 }
1956 }
1957 }
1958 }
1959
1960 let queue = SqsQueue {
1961 queue_name: queue_name.to_string(),
1962 queue_url: queue_url.clone(),
1963 arn: arn.clone(),
1964 created_at: Utc::now(),
1965 messages: std::collections::VecDeque::new(),
1966 inflight: Vec::new(),
1967 attributes,
1968 is_fifo,
1969 dedup_cache: std::collections::BTreeMap::new(),
1970 redrive_policy: None,
1971 tags: std::collections::BTreeMap::new(),
1972 next_sequence_number: 0,
1973 permission_labels: Vec::new(),
1974 receipt_handle_map: std::collections::BTreeMap::new(),
1975 };
1976
1977 state
1978 .name_to_url
1979 .insert(queue_name.to_string(), queue_url.clone());
1980 state.queues.insert(queue_url.clone(), queue);
1981
1982 Ok(ProvisionResult::new(queue_url.clone())
1983 .with("Arn", arn)
1984 .with("QueueName", queue_name)
1985 .with("QueueUrl", queue_url))
1986 }
1987
1988 fn delete_sqs_queue(&self, physical_id: &str) -> Result<(), String> {
1989 let mut __sqs_mas = self.sqs_state.write();
1990 let state = __sqs_mas.get_or_create(&self.account_id);
1991 if let Some(queue) = state.queues.remove(physical_id) {
1992 state.name_to_url.remove(&queue.queue_name);
1993 }
1994 Ok(())
1995 }
1996
1997 fn create_sns_topic(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
2000 let props = &resource.properties;
2001 let topic_name = props
2002 .get("TopicName")
2003 .and_then(|v| v.as_str())
2004 .unwrap_or(&resource.logical_id);
2005
2006 let mut __sns_mas = self.sns_state.write();
2007 let state = __sns_mas.get_or_create(&self.account_id);
2008 let topic_arn = format!(
2009 "arn:aws:sns:{}:{}:{}",
2010 state.region, state.account_id, topic_name
2011 );
2012
2013 let topic = SnsTopic {
2014 topic_arn: topic_arn.clone(),
2015 name: topic_name.to_string(),
2016 attributes: BTreeMap::new(),
2017 tags: Vec::new(),
2018 is_fifo: topic_name.ends_with(".fifo"),
2019 created_at: Utc::now(),
2020 subscriptions_deleted: 0,
2021 };
2022
2023 state.topics.insert(topic_arn.clone(), topic);
2024 Ok(ProvisionResult::new(topic_arn.clone())
2025 .with("TopicArn", topic_arn)
2026 .with("TopicName", topic_name))
2027 }
2028
2029 fn delete_sns_topic(&self, physical_id: &str) -> Result<(), String> {
2030 let mut __sns_mas = self.sns_state.write();
2031 let state = __sns_mas.get_or_create(&self.account_id);
2032 state.topics.remove(physical_id);
2033 state
2035 .subscriptions
2036 .retain(|_, sub| sub.topic_arn != physical_id);
2037 Ok(())
2038 }
2039
2040 fn create_sns_subscription(
2043 &self,
2044 resource: &ResourceDefinition,
2045 ) -> Result<ProvisionResult, String> {
2046 let props = &resource.properties;
2047 let topic_arn = props
2048 .get("TopicArn")
2049 .and_then(|v| v.as_str())
2050 .ok_or("SNS Subscription requires TopicArn")?;
2051 let protocol = props
2052 .get("Protocol")
2053 .and_then(|v| v.as_str())
2054 .ok_or("SNS Subscription requires Protocol")?;
2055 let endpoint = props
2056 .get("Endpoint")
2057 .and_then(|v| v.as_str())
2058 .ok_or("SNS Subscription requires Endpoint")?;
2059
2060 let mut __sns_mas = self.sns_state.write();
2061 let state = __sns_mas.get_or_create(&self.account_id);
2062
2063 if !state.topics.contains_key(topic_arn) {
2065 return Err(format!("Topic ARN does not exist: {topic_arn}"));
2066 }
2067
2068 let sub_arn = format!("{}:{}", topic_arn, Uuid::new_v4());
2069
2070 let subscription = SnsSubscription {
2071 subscription_arn: sub_arn.clone(),
2072 topic_arn: topic_arn.to_string(),
2073 protocol: protocol.to_string(),
2074 endpoint: endpoint.to_string(),
2075 owner: state.account_id.clone(),
2076 attributes: BTreeMap::new(),
2077 confirmed: true,
2078 confirmation_token: None,
2079 };
2080
2081 state.subscriptions.insert(sub_arn.clone(), subscription);
2082 Ok(ProvisionResult::new(sub_arn.clone()).with("Arn", sub_arn))
2083 }
2084
2085 fn delete_sns_subscription(&self, physical_id: &str) -> Result<(), String> {
2086 let mut __sns_mas = self.sns_state.write();
2087 let state = __sns_mas.get_or_create(&self.account_id);
2088 state.subscriptions.remove(physical_id);
2089 Ok(())
2090 }
2091
2092 fn create_ssm_parameter(
2095 &self,
2096 resource: &ResourceDefinition,
2097 ) -> Result<ProvisionResult, String> {
2098 let props = &resource.properties;
2099 let name = props
2100 .get("Name")
2101 .and_then(|v| v.as_str())
2102 .ok_or("SSM Parameter requires Name")?;
2103 let value = props
2104 .get("Value")
2105 .and_then(|v| v.as_str())
2106 .ok_or("SSM Parameter requires Value")?;
2107 let param_type = props
2108 .get("Type")
2109 .and_then(|v| v.as_str())
2110 .unwrap_or("String");
2111
2112 let mut accounts = self.ssm_state.write();
2113 let state = accounts.get_or_create(&self.account_id);
2114 let arn = format!(
2115 "arn:aws:ssm:{}:{}:parameter{}",
2116 self.region,
2117 self.account_id,
2118 if name.starts_with('/') {
2119 name.to_string()
2120 } else {
2121 format!("/{name}")
2122 }
2123 );
2124
2125 let parameter = SsmParameter {
2126 name: name.to_string(),
2127 value: value.to_string(),
2128 param_type: param_type.to_string(),
2129 version: 1,
2130 arn: arn.clone(),
2131 last_modified: Utc::now(),
2132 history: Vec::new(),
2133 tags: BTreeMap::new(),
2134 labels: BTreeMap::new(),
2135 description: props
2136 .get("Description")
2137 .and_then(|v| v.as_str())
2138 .map(|s| s.to_string()),
2139 allowed_pattern: None,
2140 key_id: None,
2141 data_type: "text".to_string(),
2142 tier: "Standard".to_string(),
2143 policies: None,
2144 expiration_notified: false,
2145 no_change_notified: false,
2146 };
2147
2148 state.parameters.insert(name.to_string(), parameter);
2149 Ok(ProvisionResult::new(name)
2150 .with("Type", param_type)
2151 .with("Value", value))
2152 }
2153
2154 fn delete_ssm_parameter(&self, physical_id: &str) -> Result<(), String> {
2155 let mut accounts = self.ssm_state.write();
2156 let state = accounts.get_or_create(&self.account_id);
2157 state.parameters.remove(physical_id);
2158 Ok(())
2159 }
2160
2161 fn create_iam_role(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
2164 let props = &resource.properties;
2165 let role_name = props
2166 .get("RoleName")
2167 .and_then(|v| v.as_str())
2168 .unwrap_or(&resource.logical_id);
2169
2170 let assume_role_policy = props
2171 .get("AssumeRolePolicyDocument")
2172 .map(|v| {
2173 if v.is_string() {
2174 v.as_str().unwrap().to_string()
2175 } else {
2176 serde_json::to_string(v).unwrap_or_default()
2177 }
2178 })
2179 .unwrap_or_default();
2180
2181 let path = props.get("Path").and_then(|v| v.as_str()).unwrap_or("/");
2182
2183 let mut accounts = self.iam_state.write();
2184 let state = accounts.get_or_create(&self.account_id);
2185 let role_id = format!(
2186 "FKIA{}",
2187 &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2188 );
2189 let arn = format!(
2190 "arn:aws:iam::{}:role{}{}",
2191 state.account_id,
2192 if path == "/" { "/" } else { path },
2193 role_name
2194 );
2195
2196 let role = IamRole {
2197 role_name: role_name.to_string(),
2198 role_id: role_id.clone(),
2199 arn: arn.clone(),
2200 path: path.to_string(),
2201 assume_role_policy_document: assume_role_policy,
2202 created_at: Utc::now(),
2203 description: props
2204 .get("Description")
2205 .and_then(|v| v.as_str())
2206 .map(|s| s.to_string()),
2207 max_session_duration: 3600,
2208 tags: Vec::new(),
2209 permissions_boundary: None,
2210 };
2211
2212 state.roles.insert(role_name.to_string(), role);
2213 Ok(ProvisionResult::new(arn.clone())
2214 .with("Arn", arn)
2215 .with("RoleId", role_id))
2216 }
2217
2218 fn delete_iam_role(&self, physical_id: &str) -> Result<(), String> {
2219 let mut accounts = self.iam_state.write();
2220 let state = accounts.get_or_create(&self.account_id);
2221 let role_name = state
2223 .roles
2224 .iter()
2225 .find(|(_, r)| r.arn == physical_id)
2226 .map(|(name, _)| name.clone());
2227 if let Some(name) = role_name {
2228 state.roles.remove(&name);
2229 state.role_policies.remove(&name);
2230 state.role_inline_policies.remove(&name);
2231 }
2232 Ok(())
2233 }
2234
2235 fn create_iam_policy(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
2238 let props = &resource.properties;
2239 let policy_name = props
2240 .get("PolicyName")
2241 .and_then(|v| v.as_str())
2242 .unwrap_or(&resource.logical_id);
2243
2244 let policy_document = props
2245 .get("PolicyDocument")
2246 .map(|v| {
2247 if v.is_string() {
2248 v.as_str().unwrap().to_string()
2249 } else {
2250 serde_json::to_string(v).unwrap_or_default()
2251 }
2252 })
2253 .unwrap_or_default();
2254
2255 let path = props.get("Path").and_then(|v| v.as_str()).unwrap_or("/");
2256
2257 let mut accounts = self.iam_state.write();
2258 let state = accounts.get_or_create(&self.account_id);
2259 let policy_id = format!(
2260 "FSIA{}",
2261 &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2262 );
2263 let arn = format!(
2264 "arn:aws:iam::{}:policy{}{}",
2265 state.account_id,
2266 if path == "/" { "/" } else { path },
2267 policy_name
2268 );
2269
2270 let now = Utc::now();
2271 let policy = IamPolicy {
2272 policy_name: policy_name.to_string(),
2273 policy_id,
2274 arn: arn.clone(),
2275 path: path.to_string(),
2276 description: props
2277 .get("Description")
2278 .and_then(|v| v.as_str())
2279 .unwrap_or("")
2280 .to_string(),
2281 created_at: now,
2282 tags: Vec::new(),
2283 default_version_id: "v1".to_string(),
2284 versions: vec![PolicyVersion {
2285 version_id: "v1".to_string(),
2286 document: policy_document,
2287 is_default: true,
2288 created_at: now,
2289 }],
2290 next_version_num: 2,
2291 attachment_count: 0,
2292 };
2293
2294 state.policies.insert(arn.clone(), policy);
2295 Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
2296 }
2297
2298 fn delete_iam_policy(&self, physical_id: &str) -> Result<(), String> {
2299 let mut accounts = self.iam_state.write();
2300 let state = accounts.get_or_create(&self.account_id);
2301 state.policies.remove(physical_id);
2302 Ok(())
2303 }
2304
2305 fn create_iam_user(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
2306 let props = &resource.properties;
2307 let user_name = props
2308 .get("UserName")
2309 .and_then(|v| v.as_str())
2310 .unwrap_or(&resource.logical_id)
2311 .to_string();
2312 let path = props
2313 .get("Path")
2314 .and_then(|v| v.as_str())
2315 .unwrap_or("/")
2316 .to_string();
2317 let permissions_boundary = props
2318 .get("PermissionsBoundary")
2319 .and_then(|v| v.as_str())
2320 .map(|s| s.to_string());
2321 let tags = parse_iam_tags(props.get("Tags"));
2322
2323 let mut accounts = self.iam_state.write();
2324 let state = accounts.get_or_create(&self.account_id);
2325 if state.users.contains_key(&user_name) {
2326 return Err(format!("User {user_name} already exists"));
2327 }
2328 let arn = format!(
2329 "arn:aws:iam::{}:user{}{}",
2330 state.account_id, path, user_name
2331 );
2332 let user_id = format!(
2333 "AIDA{}",
2334 &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2335 );
2336 let user = IamUser {
2337 user_name: user_name.clone(),
2338 user_id: user_id.clone(),
2339 arn: arn.clone(),
2340 path,
2341 created_at: Utc::now(),
2342 tags,
2343 permissions_boundary,
2344 };
2345 state.users.insert(user_name.clone(), user);
2346
2347 if let Some(policies) = props.get("Policies").and_then(|v| v.as_array()) {
2349 let inline = state
2350 .user_inline_policies
2351 .entry(user_name.clone())
2352 .or_default();
2353 for p in policies {
2354 if let (Some(n), Some(doc)) = (
2355 p.get("PolicyName").and_then(|v| v.as_str()),
2356 p.get("PolicyDocument"),
2357 ) {
2358 let document = if doc.is_string() {
2359 doc.as_str().unwrap_or("").to_string()
2360 } else {
2361 serde_json::to_string(doc).unwrap_or_default()
2362 };
2363 inline.insert(n.to_string(), document);
2364 }
2365 }
2366 }
2367 if let Some(arns) = props.get("ManagedPolicyArns").and_then(|v| v.as_array()) {
2368 let attached = state.user_policies.entry(user_name.clone()).or_default();
2369 for a in arns {
2370 if let Some(s) = a.as_str() {
2371 if !attached.contains(&s.to_string()) {
2372 attached.push(s.to_string());
2373 }
2374 }
2375 }
2376 }
2377 if let Some(groups) = props.get("Groups").and_then(|v| v.as_array()) {
2378 for g in groups {
2379 if let Some(g_name) = g.as_str() {
2380 if let Some(group) = state.groups.get_mut(g_name) {
2381 if !group.members.iter().any(|m| m == &user_name) {
2382 group.members.push(user_name.clone());
2383 }
2384 }
2385 }
2386 }
2387 }
2388
2389 Ok(ProvisionResult::new(user_name).with("Arn", arn))
2390 }
2391
2392 fn delete_iam_user(&self, physical_id: &str) -> Result<(), String> {
2393 let mut accounts = self.iam_state.write();
2394 let state = accounts.get_or_create(&self.account_id);
2395 state.users.remove(physical_id);
2396 state.user_inline_policies.remove(physical_id);
2397 state.user_policies.remove(physical_id);
2398 state.access_keys.remove(physical_id);
2399 for group in state.groups.values_mut() {
2400 group.members.retain(|m| m != physical_id);
2401 }
2402 Ok(())
2403 }
2404
2405 fn create_iam_group(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
2406 let props = &resource.properties;
2407 let group_name = props
2408 .get("GroupName")
2409 .and_then(|v| v.as_str())
2410 .unwrap_or(&resource.logical_id)
2411 .to_string();
2412 let path = props
2413 .get("Path")
2414 .and_then(|v| v.as_str())
2415 .unwrap_or("/")
2416 .to_string();
2417
2418 let mut accounts = self.iam_state.write();
2419 let state = accounts.get_or_create(&self.account_id);
2420 if state.groups.contains_key(&group_name) {
2421 return Err(format!("Group {group_name} already exists"));
2422 }
2423 let arn = format!(
2424 "arn:aws:iam::{}:group{}{}",
2425 state.account_id, path, group_name
2426 );
2427 let group_id = format!(
2428 "AGPA{}",
2429 &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2430 );
2431 let mut inline_policies: BTreeMap<String, String> = BTreeMap::new();
2432 if let Some(policies) = props.get("Policies").and_then(|v| v.as_array()) {
2433 for p in policies {
2434 if let (Some(n), Some(doc)) = (
2435 p.get("PolicyName").and_then(|v| v.as_str()),
2436 p.get("PolicyDocument"),
2437 ) {
2438 let document = if doc.is_string() {
2439 doc.as_str().unwrap_or("").to_string()
2440 } else {
2441 serde_json::to_string(doc).unwrap_or_default()
2442 };
2443 inline_policies.insert(n.to_string(), document);
2444 }
2445 }
2446 }
2447 let mut attached_policies: Vec<String> = Vec::new();
2448 if let Some(arns) = props.get("ManagedPolicyArns").and_then(|v| v.as_array()) {
2449 for a in arns {
2450 if let Some(s) = a.as_str() {
2451 attached_policies.push(s.to_string());
2452 }
2453 }
2454 }
2455 state.groups.insert(
2456 group_name.clone(),
2457 IamGroup {
2458 group_name: group_name.clone(),
2459 group_id,
2460 arn: arn.clone(),
2461 path,
2462 created_at: Utc::now(),
2463 members: Vec::new(),
2464 inline_policies,
2465 attached_policies,
2466 },
2467 );
2468
2469 Ok(ProvisionResult::new(group_name).with("Arn", arn))
2470 }
2471
2472 fn delete_iam_group(&self, physical_id: &str) -> Result<(), String> {
2473 let mut accounts = self.iam_state.write();
2474 let state = accounts.get_or_create(&self.account_id);
2475 state.groups.remove(physical_id);
2476 Ok(())
2477 }
2478
2479 fn create_iam_managed_policy(
2480 &self,
2481 resource: &ResourceDefinition,
2482 ) -> Result<ProvisionResult, String> {
2483 let props = &resource.properties;
2486 let policy_name = props
2487 .get("ManagedPolicyName")
2488 .and_then(|v| v.as_str())
2489 .unwrap_or(&resource.logical_id)
2490 .to_string();
2491 let policy_document = props
2492 .get("PolicyDocument")
2493 .map(|v| {
2494 if v.is_string() {
2495 v.as_str().unwrap_or("").to_string()
2496 } else {
2497 serde_json::to_string(v).unwrap_or_default()
2498 }
2499 })
2500 .unwrap_or_default();
2501 let path = props
2502 .get("Path")
2503 .and_then(|v| v.as_str())
2504 .unwrap_or("/")
2505 .to_string();
2506 let description = props
2507 .get("Description")
2508 .and_then(|v| v.as_str())
2509 .unwrap_or("")
2510 .to_string();
2511
2512 let mut accounts = self.iam_state.write();
2513 let state = accounts.get_or_create(&self.account_id);
2514 let arn = format!(
2515 "arn:aws:iam::{}:policy{}{}",
2516 state.account_id,
2517 if path == "/" { "/" } else { path.as_str() },
2518 policy_name
2519 );
2520 if state.policies.contains_key(&arn) {
2521 return Err(format!("Managed policy {policy_name} already exists"));
2522 }
2523 let policy_id = format!(
2524 "ANPA{}",
2525 &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2526 );
2527 let now = Utc::now();
2528 state.policies.insert(
2529 arn.clone(),
2530 IamPolicy {
2531 policy_name,
2532 policy_id,
2533 arn: arn.clone(),
2534 path,
2535 description,
2536 created_at: now,
2537 tags: Vec::new(),
2538 default_version_id: "v1".to_string(),
2539 versions: vec![PolicyVersion {
2540 version_id: "v1".to_string(),
2541 document: policy_document,
2542 is_default: true,
2543 created_at: now,
2544 }],
2545 next_version_num: 2,
2546 attachment_count: 0,
2547 },
2548 );
2549
2550 if let Some(users) = props.get("Users").and_then(|v| v.as_array()) {
2552 for u in users {
2553 if let Some(name) = u.as_str() {
2554 let attached = state.user_policies.entry(name.to_string()).or_default();
2555 if !attached.contains(&arn) {
2556 attached.push(arn.clone());
2557 }
2558 }
2559 }
2560 }
2561 if let Some(groups) = props.get("Groups").and_then(|v| v.as_array()) {
2562 for g in groups {
2563 if let Some(name) = g.as_str() {
2564 if let Some(group) = state.groups.get_mut(name) {
2565 if !group.attached_policies.contains(&arn) {
2566 group.attached_policies.push(arn.clone());
2567 }
2568 }
2569 }
2570 }
2571 }
2572 if let Some(roles) = props.get("Roles").and_then(|v| v.as_array()) {
2573 for r in roles {
2574 if let Some(name) = r.as_str() {
2575 let attached = state.role_policies.entry(name.to_string()).or_default();
2576 if !attached.contains(&arn) {
2577 attached.push(arn.clone());
2578 }
2579 }
2580 }
2581 }
2582
2583 Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
2584 }
2585
2586 fn delete_iam_managed_policy(&self, physical_id: &str) -> Result<(), String> {
2587 let mut accounts = self.iam_state.write();
2588 let state = accounts.get_or_create(&self.account_id);
2589 state.policies.remove(physical_id);
2590 for arns in state.user_policies.values_mut() {
2591 arns.retain(|a| a != physical_id);
2592 }
2593 for arns in state.role_policies.values_mut() {
2594 arns.retain(|a| a != physical_id);
2595 }
2596 for group in state.groups.values_mut() {
2597 group.attached_policies.retain(|a| a != physical_id);
2598 }
2599 Ok(())
2600 }
2601
2602 fn create_iam_user_to_group_addition(
2603 &self,
2604 resource: &ResourceDefinition,
2605 ) -> Result<ProvisionResult, String> {
2606 let props = &resource.properties;
2607 let group_name = props
2608 .get("GroupName")
2609 .and_then(|v| v.as_str())
2610 .ok_or_else(|| "GroupName is required".to_string())?
2611 .to_string();
2612 let users: Vec<String> = props
2613 .get("Users")
2614 .and_then(|v| v.as_array())
2615 .map(|arr| {
2616 arr.iter()
2617 .filter_map(|u| u.as_str().map(|s| s.to_string()))
2618 .collect()
2619 })
2620 .unwrap_or_default();
2621
2622 let mut accounts = self.iam_state.write();
2623 let state = accounts.get_or_create(&self.account_id);
2624 let group = state
2625 .groups
2626 .get_mut(&group_name)
2627 .ok_or_else(|| format!("Group {group_name} does not exist"))?;
2628 for u in &users {
2629 if !group.members.iter().any(|m| m == u) {
2630 group.members.push(u.clone());
2631 }
2632 }
2633
2634 let physical_id = format!("{group_name}|{}", users.join(","));
2636 Ok(ProvisionResult::new(physical_id))
2637 }
2638
2639 fn delete_iam_user_to_group_addition(&self, physical_id: &str) -> Result<(), String> {
2640 let mut accounts = self.iam_state.write();
2641 let state = accounts.get_or_create(&self.account_id);
2642 if let Some((group_name, users)) = physical_id.split_once('|') {
2643 if let Some(group) = state.groups.get_mut(group_name) {
2644 let to_remove: Vec<&str> = users.split(',').filter(|s| !s.is_empty()).collect();
2645 group.members.retain(|m| !to_remove.iter().any(|u| u == m));
2646 }
2647 }
2648 Ok(())
2649 }
2650
2651 fn create_iam_access_key(
2652 &self,
2653 resource: &ResourceDefinition,
2654 ) -> Result<ProvisionResult, String> {
2655 let props = &resource.properties;
2656 let user_name = props
2657 .get("UserName")
2658 .and_then(|v| v.as_str())
2659 .ok_or_else(|| "UserName is required".to_string())?
2660 .to_string();
2661 let status = props
2662 .get("Status")
2663 .and_then(|v| v.as_str())
2664 .unwrap_or("Active")
2665 .to_string();
2666
2667 let mut accounts = self.iam_state.write();
2668 let state = accounts.get_or_create(&self.account_id);
2669 if !state.users.contains_key(&user_name) {
2670 return Err(format!("User {user_name} does not exist"));
2671 }
2672 let access_key_id = format!(
2673 "AKIA{}",
2674 &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2675 );
2676 let secret_access_key: String = Uuid::new_v4()
2677 .to_string()
2678 .replace('-', "")
2679 .chars()
2680 .take(40)
2681 .collect();
2682 state
2683 .access_keys
2684 .entry(user_name.clone())
2685 .or_default()
2686 .push(IamAccessKey {
2687 access_key_id: access_key_id.clone(),
2688 secret_access_key: secret_access_key.clone(),
2689 user_name: user_name.clone(),
2690 status,
2691 created_at: Utc::now(),
2692 });
2693
2694 Ok(ProvisionResult::new(access_key_id.clone()).with("SecretAccessKey", secret_access_key))
2695 }
2696
2697 fn delete_iam_access_key(&self, physical_id: &str) -> Result<(), String> {
2698 let mut accounts = self.iam_state.write();
2699 let state = accounts.get_or_create(&self.account_id);
2700 for keys in state.access_keys.values_mut() {
2701 keys.retain(|k| k.access_key_id != physical_id);
2702 }
2703 Ok(())
2704 }
2705
2706 fn create_iam_instance_profile(
2707 &self,
2708 resource: &ResourceDefinition,
2709 ) -> Result<ProvisionResult, String> {
2710 let props = &resource.properties;
2711 let name = props
2712 .get("InstanceProfileName")
2713 .and_then(|v| v.as_str())
2714 .unwrap_or(&resource.logical_id)
2715 .to_string();
2716 let path = props
2717 .get("Path")
2718 .and_then(|v| v.as_str())
2719 .unwrap_or("/")
2720 .to_string();
2721 let roles: Vec<String> = props
2726 .get("Roles")
2727 .and_then(|v| v.as_array())
2728 .map(|arr| {
2729 arr.iter()
2730 .filter_map(|r| r.as_str())
2731 .map(|s| {
2732 if let Some(rest) = s.strip_prefix("arn:aws:iam::") {
2733 rest.split(":role/")
2734 .nth(1)
2735 .map(|name| name.to_string())
2736 .unwrap_or_else(|| s.to_string())
2737 } else {
2738 s.to_string()
2739 }
2740 })
2741 .collect()
2742 })
2743 .unwrap_or_default();
2744
2745 let mut accounts = self.iam_state.write();
2746 let state = accounts.get_or_create(&self.account_id);
2747 if state.instance_profiles.contains_key(&name) {
2748 return Err(format!("InstanceProfile {name} already exists"));
2749 }
2750 for role_name in &roles {
2755 if !state.roles.contains_key(role_name) {
2756 return Err(format!(
2757 "InstanceProfile {name}: referenced role {role_name} not yet provisioned"
2758 ));
2759 }
2760 }
2761 let arn = format!(
2762 "arn:aws:iam::{}:instance-profile{}{}",
2763 state.account_id, path, name
2764 );
2765 let id = format!(
2766 "AIPA{}",
2767 &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2768 );
2769 state.instance_profiles.insert(
2770 name.clone(),
2771 IamInstanceProfile {
2772 instance_profile_name: name.clone(),
2773 instance_profile_id: id,
2774 arn: arn.clone(),
2775 path,
2776 created_at: Utc::now(),
2777 roles,
2778 tags: Vec::new(),
2779 },
2780 );
2781
2782 Ok(ProvisionResult::new(name).with("Arn", arn))
2783 }
2784
2785 fn delete_iam_instance_profile(&self, physical_id: &str) -> Result<(), String> {
2786 let mut accounts = self.iam_state.write();
2787 let state = accounts.get_or_create(&self.account_id);
2788 state.instance_profiles.remove(physical_id);
2789 Ok(())
2790 }
2791
2792 fn create_iam_oidc_provider(
2793 &self,
2794 resource: &ResourceDefinition,
2795 ) -> Result<ProvisionResult, String> {
2796 let props = &resource.properties;
2797 let url = props
2798 .get("Url")
2799 .and_then(|v| v.as_str())
2800 .ok_or("Url is required")?
2801 .to_string();
2802 let client_id_list: Vec<String> = props
2803 .get("ClientIdList")
2804 .and_then(|v| v.as_array())
2805 .map(|arr| {
2806 arr.iter()
2807 .filter_map(|v| v.as_str().map(String::from))
2808 .collect()
2809 })
2810 .unwrap_or_default();
2811 let thumbprint_list: Vec<String> = props
2812 .get("ThumbprintList")
2813 .and_then(|v| v.as_array())
2814 .map(|arr| {
2815 arr.iter()
2816 .filter_map(|v| v.as_str().map(String::from))
2817 .collect()
2818 })
2819 .unwrap_or_default();
2820 let url_path = url
2822 .trim_start_matches("https://")
2823 .trim_start_matches("http://")
2824 .to_string();
2825 let arn = format!(
2826 "arn:aws:iam::{}:oidc-provider/{}",
2827 self.account_id, url_path
2828 );
2829 let provider = OidcProvider {
2830 arn: arn.clone(),
2831 url,
2832 client_id_list,
2833 thumbprint_list,
2834 created_at: Utc::now(),
2835 tags: Vec::new(),
2836 };
2837 let mut accounts = self.iam_state.write();
2838 let state = accounts.get_or_create(&self.account_id);
2839 state.oidc_providers.insert(arn.clone(), provider);
2840 Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
2841 }
2842
2843 fn delete_iam_oidc_provider(&self, physical_id: &str) -> Result<(), String> {
2844 let mut accounts = self.iam_state.write();
2845 let state = accounts.get_or_create(&self.account_id);
2846 state.oidc_providers.remove(physical_id);
2847 Ok(())
2848 }
2849
2850 fn create_iam_saml_provider(
2851 &self,
2852 resource: &ResourceDefinition,
2853 ) -> Result<ProvisionResult, String> {
2854 let props = &resource.properties;
2855 let name = props
2856 .get("Name")
2857 .and_then(|v| v.as_str())
2858 .map(String::from)
2859 .unwrap_or_else(|| {
2860 let suffix = Uuid::new_v4().simple().to_string();
2861 format!("{}-{}", resource.logical_id, &suffix[..8])
2862 });
2863 let saml_metadata_document = props
2864 .get("SamlMetadataDocument")
2865 .and_then(|v| v.as_str())
2866 .ok_or("SamlMetadataDocument is required")?
2867 .to_string();
2868 let arn = format!("arn:aws:iam::{}:saml-provider/{name}", self.account_id);
2869 let now = Utc::now();
2870 let valid_until = now + chrono::Duration::days(365 * 10);
2871 let provider = SamlProvider {
2872 arn: arn.clone(),
2873 name,
2874 saml_metadata_document,
2875 created_at: now,
2876 valid_until,
2877 tags: Vec::new(),
2878 };
2879 let mut accounts = self.iam_state.write();
2880 let state = accounts.get_or_create(&self.account_id);
2881 state.saml_providers.insert(arn.clone(), provider);
2882 Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
2883 }
2884
2885 fn delete_iam_saml_provider(&self, physical_id: &str) -> Result<(), String> {
2886 let mut accounts = self.iam_state.write();
2887 let state = accounts.get_or_create(&self.account_id);
2888 state.saml_providers.remove(physical_id);
2889 Ok(())
2890 }
2891
2892 fn create_iam_service_linked_role(
2893 &self,
2894 resource: &ResourceDefinition,
2895 ) -> Result<ProvisionResult, String> {
2896 let props = &resource.properties;
2897 let aws_service_name = props
2898 .get("AWSServiceName")
2899 .and_then(|v| v.as_str())
2900 .ok_or("AWSServiceName is required")?
2901 .to_string();
2902 let custom_suffix = props
2903 .get("CustomSuffix")
2904 .and_then(|v| v.as_str())
2905 .map(String::from);
2906 let description = props
2907 .get("Description")
2908 .and_then(|v| v.as_str())
2909 .unwrap_or("")
2910 .to_string();
2911 let service_short = aws_service_name.split('.').next().unwrap_or("Service");
2913 let role_name = match &custom_suffix {
2914 Some(s) => format!("AWSServiceRoleFor{service_short}_{s}"),
2915 None => format!("AWSServiceRoleFor{service_short}"),
2916 };
2917 let path = format!("/aws-service-role/{aws_service_name}/");
2918 let arn = format!("arn:aws:iam::{}:role{path}{role_name}", self.account_id);
2919 let assume_role_policy_document = serde_json::json!({
2921 "Version": "2012-10-17",
2922 "Statement": [{
2923 "Effect": "Allow",
2924 "Principal": {"Service": aws_service_name.clone()},
2925 "Action": "sts:AssumeRole"
2926 }]
2927 })
2928 .to_string();
2929 let role_id_suffix = Uuid::new_v4().simple().to_string();
2930 let role = IamRole {
2931 role_name: role_name.clone(),
2932 role_id: format!("AROA{}", role_id_suffix[..16].to_uppercase()),
2933 arn: arn.clone(),
2934 path,
2935 assume_role_policy_document,
2936 created_at: Utc::now(),
2937 description: Some(description),
2938 max_session_duration: 3600,
2939 tags: Vec::new(),
2940 permissions_boundary: None,
2941 };
2942 let mut accounts = self.iam_state.write();
2943 let state = accounts.get_or_create(&self.account_id);
2944 state.roles.insert(role_name.clone(), role);
2945 Ok(ProvisionResult::new(role_name)
2946 .with("Arn", arn)
2947 .with("RoleId", String::new()))
2948 }
2949
2950 fn delete_iam_service_linked_role(&self, physical_id: &str) -> Result<(), String> {
2951 let mut accounts = self.iam_state.write();
2952 let state = accounts.get_or_create(&self.account_id);
2953 state.roles.remove(physical_id);
2954 Ok(())
2955 }
2956
2957 fn create_iam_virtual_mfa_device(
2958 &self,
2959 resource: &ResourceDefinition,
2960 ) -> Result<ProvisionResult, String> {
2961 let props = &resource.properties;
2962 let name = props
2963 .get("VirtualMfaDeviceName")
2964 .and_then(|v| v.as_str())
2965 .ok_or("VirtualMfaDeviceName is required")?
2966 .to_string();
2967 let path = props
2968 .get("Path")
2969 .and_then(|v| v.as_str())
2970 .unwrap_or("/")
2971 .to_string();
2972 let serial_number = format!("arn:aws:iam::{}:mfa{}{name}", self.account_id, path);
2973 let seed = format!("BASE32SEED{}", Uuid::new_v4().simple());
2976 let user = props
2977 .get("Users")
2978 .and_then(|v| v.as_array())
2979 .and_then(|arr| arr.first())
2980 .and_then(|v| v.as_str())
2981 .map(String::from);
2982 let device = VirtualMfaDevice {
2983 serial_number: serial_number.clone(),
2984 base32_string_seed: seed,
2985 qr_code_png: String::new(),
2986 enable_date: user.as_ref().map(|_| Utc::now()),
2987 user,
2988 tags: Vec::new(),
2989 };
2990 let mut accounts = self.iam_state.write();
2991 let state = accounts.get_or_create(&self.account_id);
2992 state
2993 .virtual_mfa_devices
2994 .insert(serial_number.clone(), device);
2995 Ok(ProvisionResult::new(serial_number.clone()).with("SerialNumber", serial_number))
2996 }
2997
2998 fn delete_iam_virtual_mfa_device(&self, physical_id: &str) -> Result<(), String> {
2999 let mut accounts = self.iam_state.write();
3000 let state = accounts.get_or_create(&self.account_id);
3001 state.virtual_mfa_devices.remove(physical_id);
3002 Ok(())
3003 }
3004
3005 fn create_s3_bucket(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
3008 let props = &resource.properties;
3009 let bucket_name = props
3010 .get("BucketName")
3011 .and_then(|v| v.as_str())
3012 .unwrap_or(&resource.logical_id);
3013
3014 let mut __s3_mas = self.s3_state.write();
3015 let state = __s3_mas.get_or_create(&self.account_id);
3016 let region = state.region.clone();
3017 let bucket = S3Bucket::new(bucket_name, &state.region, &state.account_id);
3018 state.buckets.insert(bucket_name.to_string(), bucket);
3019
3020 let arn = format!("arn:aws:s3:::{bucket_name}");
3021 let domain_name = format!("{bucket_name}.s3.amazonaws.com");
3022 let regional_domain_name = format!("{bucket_name}.s3.{region}.amazonaws.com");
3023 let dual_stack_domain_name = format!("{bucket_name}.s3.dualstack.{region}.amazonaws.com");
3024 let website_url = format!("http://{bucket_name}.s3-website-{region}.amazonaws.com");
3025 Ok(ProvisionResult::new(bucket_name)
3026 .with("Arn", arn)
3027 .with("DomainName", domain_name)
3028 .with("RegionalDomainName", regional_domain_name)
3029 .with("DualStackDomainName", dual_stack_domain_name)
3030 .with("WebsiteURL", website_url))
3031 }
3032
3033 fn delete_s3_bucket(&self, physical_id: &str) -> Result<(), String> {
3034 let mut __s3_mas = self.s3_state.write();
3035 let state = __s3_mas.get_or_create(&self.account_id);
3036 state.buckets.remove(physical_id);
3037 Ok(())
3038 }
3039
3040 fn create_eventbridge_rule(
3043 &self,
3044 resource: &ResourceDefinition,
3045 ) -> Result<ProvisionResult, String> {
3046 let props = &resource.properties;
3047 let rule_name = props
3048 .get("Name")
3049 .and_then(|v| v.as_str())
3050 .unwrap_or(&resource.logical_id);
3051 let event_bus_name = props
3052 .get("EventBusName")
3053 .and_then(|v| v.as_str())
3054 .unwrap_or("default");
3055
3056 let mut eb_accounts = self.eventbridge_state.write();
3057 let state = eb_accounts.get_or_create(&self.account_id);
3058
3059 if !state.buses.contains_key(event_bus_name) {
3061 return Err(format!("Event bus does not exist: {event_bus_name}"));
3062 }
3063
3064 let arn = if event_bus_name == "default" {
3065 format!(
3066 "arn:aws:events:{}:{}:rule/{}",
3067 state.region, state.account_id, rule_name
3068 )
3069 } else {
3070 format!(
3071 "arn:aws:events:{}:{}:rule/{}/{}",
3072 state.region, state.account_id, event_bus_name, rule_name
3073 )
3074 };
3075
3076 let rule = EventRule {
3077 name: rule_name.to_string(),
3078 arn: arn.clone(),
3079 event_bus_name: event_bus_name.to_string(),
3080 event_pattern: props.get("EventPattern").map(|v| {
3081 if v.is_string() {
3082 v.as_str().unwrap().to_string()
3083 } else {
3084 serde_json::to_string(v).unwrap_or_default()
3085 }
3086 }),
3087 schedule_expression: props
3088 .get("ScheduleExpression")
3089 .and_then(|v| v.as_str())
3090 .map(|s| s.to_string()),
3091 state: props
3092 .get("State")
3093 .and_then(|v| v.as_str())
3094 .unwrap_or("ENABLED")
3095 .to_string(),
3096 description: props
3097 .get("Description")
3098 .and_then(|v| v.as_str())
3099 .map(|s| s.to_string()),
3100 role_arn: props
3101 .get("RoleArn")
3102 .and_then(|v| v.as_str())
3103 .map(|s| s.to_string()),
3104 managed_by: None,
3105 created_by: None,
3106 targets: Vec::new(),
3107 tags: std::collections::BTreeMap::new(),
3108 last_fired: None,
3109 };
3110
3111 state
3112 .rules
3113 .insert((event_bus_name.to_string(), rule_name.to_string()), rule);
3114 Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
3115 }
3116
3117 fn delete_eventbridge_rule(&self, physical_id: &str) -> Result<(), String> {
3118 let mut eb_accounts = self.eventbridge_state.write();
3119 let state = eb_accounts.default_mut();
3120 let key = state
3122 .rules
3123 .iter()
3124 .find(|(_, r)| r.arn == physical_id)
3125 .map(|(k, _)| k.clone());
3126 if let Some(k) = key {
3127 state.rules.remove(&k);
3128 }
3129 Ok(())
3130 }
3131
3132 fn create_eventbridge_connection(
3133 &self,
3134 resource: &ResourceDefinition,
3135 ) -> Result<ProvisionResult, String> {
3136 let props = &resource.properties;
3137 let name = props
3138 .get("Name")
3139 .and_then(|v| v.as_str())
3140 .unwrap_or(&resource.logical_id)
3141 .to_string();
3142 let description = props
3143 .get("Description")
3144 .and_then(|v| v.as_str())
3145 .map(|s| s.to_string());
3146 let authorization_type = props
3147 .get("AuthorizationType")
3148 .and_then(|v| v.as_str())
3149 .unwrap_or("API_KEY")
3150 .to_string();
3151 let auth_parameters = props
3152 .get("AuthParameters")
3153 .cloned()
3154 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
3155
3156 let mut eb_accounts = self.eventbridge_state.write();
3157 let state = eb_accounts.get_or_create(&self.account_id);
3158 if state.connections.contains_key(&name) {
3159 return Err(format!("Connection {name} already exists"));
3160 }
3161 let now = Utc::now();
3162 let arn = format!(
3163 "arn:aws:events:{}:{}:connection/{}/{}",
3164 state.region,
3165 state.account_id,
3166 name,
3167 Uuid::new_v4().as_simple()
3168 );
3169 let secret_arn = format!(
3170 "arn:aws:secretsmanager:{}:{}:secret:events!connection/{}-{}",
3171 state.region,
3172 state.account_id,
3173 name,
3174 Uuid::new_v4().as_simple()
3175 );
3176 let connection = Connection {
3177 name: name.clone(),
3178 arn: arn.clone(),
3179 description,
3180 authorization_type,
3181 auth_parameters,
3182 connection_state: "AUTHORIZED".to_string(),
3183 secret_arn: secret_arn.clone(),
3184 creation_time: now,
3185 last_modified_time: now,
3186 last_authorized_time: now,
3187 };
3188 state.connections.insert(name.clone(), connection);
3189
3190 Ok(ProvisionResult::new(name)
3191 .with("Arn", arn)
3192 .with("SecretArn", secret_arn))
3193 }
3194
3195 fn delete_eventbridge_connection(&self, physical_id: &str) -> Result<(), String> {
3196 let mut eb_accounts = self.eventbridge_state.write();
3197 let state = eb_accounts.get_or_create(&self.account_id);
3198 state.connections.remove(physical_id);
3199 Ok(())
3200 }
3201
3202 fn create_eventbridge_api_destination(
3203 &self,
3204 resource: &ResourceDefinition,
3205 ) -> Result<ProvisionResult, String> {
3206 let props = &resource.properties;
3207 let name = props
3208 .get("Name")
3209 .and_then(|v| v.as_str())
3210 .unwrap_or(&resource.logical_id)
3211 .to_string();
3212 let description = props
3213 .get("Description")
3214 .and_then(|v| v.as_str())
3215 .map(|s| s.to_string());
3216 let connection_arn = props
3217 .get("ConnectionArn")
3218 .and_then(|v| v.as_str())
3219 .ok_or_else(|| "ConnectionArn is required".to_string())?
3220 .to_string();
3221 let invocation_endpoint = props
3222 .get("InvocationEndpoint")
3223 .and_then(|v| v.as_str())
3224 .ok_or_else(|| "InvocationEndpoint is required".to_string())?
3225 .to_string();
3226 let http_method = props
3227 .get("HttpMethod")
3228 .and_then(|v| v.as_str())
3229 .unwrap_or("POST")
3230 .to_string();
3231 let invocation_rate_limit_per_second = props
3232 .get("InvocationRateLimitPerSecond")
3233 .and_then(|v| v.as_i64());
3234
3235 let mut eb_accounts = self.eventbridge_state.write();
3236 let state = eb_accounts.get_or_create(&self.account_id);
3237 if state.api_destinations.contains_key(&name) {
3238 return Err(format!("ApiDestination {name} already exists"));
3239 }
3240 let now = Utc::now();
3241 let arn = format!(
3242 "arn:aws:events:{}:{}:api-destination/{}/{}",
3243 state.region,
3244 state.account_id,
3245 name,
3246 Uuid::new_v4().as_simple()
3247 );
3248 state.api_destinations.insert(
3249 name.clone(),
3250 ApiDestination {
3251 name: name.clone(),
3252 arn: arn.clone(),
3253 description,
3254 connection_arn,
3255 invocation_endpoint,
3256 http_method,
3257 invocation_rate_limit_per_second,
3258 state: "ACTIVE".to_string(),
3259 creation_time: now,
3260 last_modified_time: now,
3261 },
3262 );
3263
3264 Ok(ProvisionResult::new(name).with("Arn", arn))
3265 }
3266
3267 fn delete_eventbridge_api_destination(&self, physical_id: &str) -> Result<(), String> {
3268 let mut eb_accounts = self.eventbridge_state.write();
3269 let state = eb_accounts.get_or_create(&self.account_id);
3270 state.api_destinations.remove(physical_id);
3271 Ok(())
3272 }
3273
3274 fn create_eventbridge_archive(
3275 &self,
3276 resource: &ResourceDefinition,
3277 ) -> Result<ProvisionResult, String> {
3278 let props = &resource.properties;
3279 let name = props
3280 .get("ArchiveName")
3281 .and_then(|v| v.as_str())
3282 .unwrap_or(&resource.logical_id)
3283 .to_string();
3284 let event_source_arn = props
3285 .get("SourceArn")
3286 .and_then(|v| v.as_str())
3287 .ok_or_else(|| "SourceArn is required".to_string())?
3288 .to_string();
3289 let description = props
3290 .get("Description")
3291 .and_then(|v| v.as_str())
3292 .map(|s| s.to_string());
3293 let event_pattern = props.get("EventPattern").map(|v| {
3294 if v.is_string() {
3295 v.as_str().unwrap_or("").to_string()
3296 } else {
3297 serde_json::to_string(v).unwrap_or_default()
3298 }
3299 });
3300 let retention_days = props
3301 .get("RetentionDays")
3302 .and_then(|v| v.as_i64())
3303 .unwrap_or(0);
3304
3305 let mut eb_accounts = self.eventbridge_state.write();
3306 let state = eb_accounts.get_or_create(&self.account_id);
3307 if state.archives.contains_key(&name) {
3308 return Err(format!("Archive {name} already exists"));
3309 }
3310 let arn = format!(
3311 "arn:aws:events:{}:{}:archive/{}",
3312 state.region, state.account_id, name
3313 );
3314 state.archives.insert(
3315 name.clone(),
3316 Archive {
3317 name: name.clone(),
3318 arn: arn.clone(),
3319 event_source_arn,
3320 description,
3321 event_pattern,
3322 retention_days,
3323 state: "ENABLED".to_string(),
3324 creation_time: Utc::now(),
3325 event_count: 0,
3326 size_bytes: 0,
3327 events: Vec::new(),
3328 },
3329 );
3330
3331 Ok(ProvisionResult::new(name).with("Arn", arn))
3332 }
3333
3334 fn delete_eventbridge_archive(&self, physical_id: &str) -> Result<(), String> {
3335 let mut eb_accounts = self.eventbridge_state.write();
3336 let state = eb_accounts.get_or_create(&self.account_id);
3337 state.archives.remove(physical_id);
3338 Ok(())
3339 }
3340
3341 fn create_eventbridge_event_bus(
3342 &self,
3343 resource: &ResourceDefinition,
3344 ) -> Result<ProvisionResult, String> {
3345 let props = &resource.properties;
3346 let name = props
3347 .get("Name")
3348 .and_then(|v| v.as_str())
3349 .ok_or("Name is required")?
3350 .to_string();
3351 let description = props
3352 .get("Description")
3353 .and_then(|v| v.as_str())
3354 .map(String::from);
3355 let kms_key_identifier = props
3356 .get("KmsKeyIdentifier")
3357 .and_then(|v| v.as_str())
3358 .map(String::from);
3359 let dead_letter_config = props.get("DeadLetterConfig").cloned();
3360 let policy = props.get("Policy").cloned();
3361 let arn = format!(
3362 "arn:aws:events:{}:{}:event-bus/{name}",
3363 self.region, self.account_id
3364 );
3365 let now = Utc::now();
3366 let bus = EventBus {
3367 name: name.clone(),
3368 arn: arn.clone(),
3369 tags: BTreeMap::new(),
3370 policy,
3371 description,
3372 kms_key_identifier,
3373 dead_letter_config,
3374 creation_time: now,
3375 last_modified_time: now,
3376 };
3377
3378 let mut eb_accounts = self.eventbridge_state.write();
3379 let state = eb_accounts.get_or_create(&self.account_id);
3380 state.buses.insert(name.clone(), bus);
3381
3382 Ok(ProvisionResult::new(name).with("Arn", arn))
3383 }
3384
3385 fn delete_eventbridge_event_bus(&self, physical_id: &str) -> Result<(), String> {
3386 let mut eb_accounts = self.eventbridge_state.write();
3387 let state = eb_accounts.get_or_create(&self.account_id);
3388 if physical_id == "default" {
3390 return Ok(());
3391 }
3392 state.buses.remove(physical_id);
3393 Ok(())
3394 }
3395
3396 fn create_eventbridge_event_bus_policy(
3397 &self,
3398 resource: &ResourceDefinition,
3399 ) -> Result<ProvisionResult, String> {
3400 let props = &resource.properties;
3401 let bus_name = props
3402 .get("EventBusName")
3403 .and_then(|v| v.as_str())
3404 .unwrap_or("default")
3405 .to_string();
3406 let statement = if let Some(s) = props.get("Statement") {
3410 s.clone()
3411 } else {
3412 let sid = props
3413 .get("Sid")
3414 .or_else(|| props.get("StatementId"))
3415 .and_then(|v| v.as_str())
3416 .map(String::from);
3417 let action = props
3418 .get("Action")
3419 .and_then(|v| v.as_str())
3420 .map(String::from);
3421 let principal = props.get("Principal").cloned();
3422 let condition = props.get("Condition").cloned();
3423 let mut obj = serde_json::json!({
3424 "Effect": "Allow",
3425 "Resource": format!(
3426 "arn:aws:events:{}:{}:event-bus/{bus_name}",
3427 self.region, self.account_id
3428 ),
3429 });
3430 if let (Some(sid), Some(obj)) = (sid, obj.as_object_mut()) {
3431 obj.insert("Sid".to_string(), serde_json::Value::String(sid));
3432 }
3433 if let (Some(action), Some(obj)) = (action, obj.as_object_mut()) {
3434 obj.insert("Action".to_string(), serde_json::Value::String(action));
3435 }
3436 if let (Some(principal), Some(obj)) = (principal, obj.as_object_mut()) {
3437 obj.insert("Principal".to_string(), principal);
3438 }
3439 if let (Some(condition), Some(obj)) = (condition, obj.as_object_mut()) {
3440 obj.insert("Condition".to_string(), condition);
3441 }
3442 obj
3443 };
3444
3445 let mut eb_accounts = self.eventbridge_state.write();
3446 let state = eb_accounts.get_or_create(&self.account_id);
3447 let bus = state
3448 .buses
3449 .get_mut(&bus_name)
3450 .ok_or_else(|| format!("EventBus {bus_name} not yet provisioned"))?;
3451 match bus.policy.as_mut() {
3453 Some(serde_json::Value::Object(obj)) => {
3454 if let Some(serde_json::Value::Array(arr)) = obj.get_mut("Statement") {
3455 arr.push(statement);
3456 } else {
3457 obj.insert(
3458 "Statement".to_string(),
3459 serde_json::Value::Array(vec![statement]),
3460 );
3461 }
3462 }
3463 _ => {
3464 bus.policy = Some(serde_json::json!({
3465 "Version": "2012-10-17",
3466 "Statement": [statement],
3467 }));
3468 }
3469 }
3470
3471 let pid = format!("{bus_name}|{}", Uuid::new_v4().simple());
3474 Ok(ProvisionResult::new(pid))
3475 }
3476
3477 fn delete_eventbridge_event_bus_policy(&self, physical_id: &str) -> Result<(), String> {
3478 let bus_name = physical_id
3479 .split_once('|')
3480 .map(|(b, _)| b.to_string())
3481 .unwrap_or_else(|| "default".to_string());
3482 let mut eb_accounts = self.eventbridge_state.write();
3483 let state = eb_accounts.get_or_create(&self.account_id);
3484 if let Some(bus) = state.buses.get_mut(&bus_name) {
3485 bus.policy = None;
3486 }
3487 Ok(())
3488 }
3489
3490 fn create_eventbridge_endpoint(
3491 &self,
3492 resource: &ResourceDefinition,
3493 ) -> Result<ProvisionResult, String> {
3494 let props = &resource.properties;
3495 let name = props
3496 .get("Name")
3497 .and_then(|v| v.as_str())
3498 .ok_or("Name is required")?
3499 .to_string();
3500 let description = props
3501 .get("Description")
3502 .and_then(|v| v.as_str())
3503 .map(String::from);
3504 let routing_config = props
3505 .get("RoutingConfig")
3506 .cloned()
3507 .ok_or("RoutingConfig is required")?;
3508 let replication_config = props.get("ReplicationConfig").cloned();
3509 let event_buses = props
3510 .get("EventBuses")
3511 .and_then(|v| v.as_array())
3512 .cloned()
3513 .unwrap_or_default();
3514 let role_arn = props
3515 .get("RoleArn")
3516 .and_then(|v| v.as_str())
3517 .map(String::from);
3518
3519 let endpoint_id = Uuid::new_v4().simple().to_string()[..16].to_string();
3520 let arn = format!(
3521 "arn:aws:events:{}:{}:endpoint/{name}",
3522 self.region, self.account_id
3523 );
3524 let endpoint_url = format!("https://{endpoint_id}.endpoint.events.amazonaws.com");
3525 let now = Utc::now();
3526 let endpoint = Endpoint {
3527 name: name.clone(),
3528 arn: arn.clone(),
3529 endpoint_id: endpoint_id.clone(),
3530 endpoint_url: Some(endpoint_url.clone()),
3531 description,
3532 routing_config,
3533 replication_config,
3534 event_buses,
3535 role_arn,
3536 state: "ACTIVE".to_string(),
3537 creation_time: now,
3538 last_modified_time: now,
3539 };
3540
3541 let mut eb_accounts = self.eventbridge_state.write();
3542 let state = eb_accounts.get_or_create(&self.account_id);
3543 state.endpoints.insert(name.clone(), endpoint);
3544
3545 Ok(ProvisionResult::new(name)
3546 .with("Arn", arn)
3547 .with("EndpointId", endpoint_id)
3548 .with("EndpointUrl", endpoint_url)
3549 .with("State", "ACTIVE"))
3550 }
3551
3552 fn delete_eventbridge_endpoint(&self, physical_id: &str) -> Result<(), String> {
3553 let mut eb_accounts = self.eventbridge_state.write();
3554 let state = eb_accounts.get_or_create(&self.account_id);
3555 state.endpoints.remove(physical_id);
3556 Ok(())
3557 }
3558
3559 fn create_dynamodb_table(
3562 &self,
3563 resource: &ResourceDefinition,
3564 ) -> Result<ProvisionResult, String> {
3565 let props = &resource.properties;
3566 let table_name = props
3567 .get("TableName")
3568 .and_then(|v| v.as_str())
3569 .unwrap_or(&resource.logical_id);
3570
3571 let mut key_schema = Vec::new();
3572 if let Some(ks) = props.get("KeySchema").and_then(|v| v.as_array()) {
3573 for item in ks {
3574 let attr_name = item
3575 .get("AttributeName")
3576 .and_then(|v| v.as_str())
3577 .unwrap_or("")
3578 .to_string();
3579 let key_type = item
3580 .get("KeyType")
3581 .and_then(|v| v.as_str())
3582 .unwrap_or("HASH")
3583 .to_string();
3584 key_schema.push(KeySchemaElement {
3585 attribute_name: attr_name,
3586 key_type,
3587 });
3588 }
3589 }
3590
3591 let mut attribute_definitions = Vec::new();
3592 if let Some(defs) = props.get("AttributeDefinitions").and_then(|v| v.as_array()) {
3593 for item in defs {
3594 let attr_name = item
3595 .get("AttributeName")
3596 .and_then(|v| v.as_str())
3597 .unwrap_or("")
3598 .to_string();
3599 let attr_type = item
3600 .get("AttributeType")
3601 .and_then(|v| v.as_str())
3602 .unwrap_or("S")
3603 .to_string();
3604 attribute_definitions.push(AttributeDefinition {
3605 attribute_name: attr_name,
3606 attribute_type: attr_type,
3607 });
3608 }
3609 }
3610
3611 let billing_mode = props
3612 .get("BillingMode")
3613 .and_then(|v| v.as_str())
3614 .unwrap_or("PAY_PER_REQUEST")
3615 .to_string();
3616
3617 let provisioned_throughput = if billing_mode == "PROVISIONED" {
3618 if let Some(pt) = props.get("ProvisionedThroughput") {
3619 ProvisionedThroughput {
3620 read_capacity_units: pt
3621 .get("ReadCapacityUnits")
3622 .and_then(|v| v.as_i64())
3623 .unwrap_or(5),
3624 write_capacity_units: pt
3625 .get("WriteCapacityUnits")
3626 .and_then(|v| v.as_i64())
3627 .unwrap_or(5),
3628 }
3629 } else {
3630 ProvisionedThroughput {
3631 read_capacity_units: 5,
3632 write_capacity_units: 5,
3633 }
3634 }
3635 } else {
3636 ProvisionedThroughput {
3637 read_capacity_units: 0,
3638 write_capacity_units: 0,
3639 }
3640 };
3641
3642 let (stream_enabled, stream_view_type) =
3644 if let Some(stream_spec) = props.get("StreamSpecification") {
3645 let view_type = stream_spec
3646 .get("StreamViewType")
3647 .and_then(|v| v.as_str())
3648 .map(|s| s.to_string());
3649 let enabled = stream_spec
3650 .get("StreamEnabled")
3651 .and_then(|v| v.as_bool().or_else(|| v.as_str().map(|s| s == "true")))
3652 .unwrap_or(view_type.is_some());
3654 (enabled, view_type)
3655 } else {
3656 (false, None)
3657 };
3658
3659 let deletion_protection_enabled = props
3660 .get("DeletionProtectionEnabled")
3661 .and_then(|v| v.as_bool().or_else(|| v.as_str().map(|s| s == "true")))
3662 .unwrap_or(false);
3663
3664 let on_demand_throughput = props
3665 .get("OnDemandThroughput")
3666 .map(|odt| OnDemandThroughput {
3667 max_read_request_units: odt
3668 .get("MaxReadRequestUnits")
3669 .and_then(|v| v.as_i64())
3670 .unwrap_or(-1),
3671 max_write_request_units: odt
3672 .get("MaxWriteRequestUnits")
3673 .and_then(|v| v.as_i64())
3674 .unwrap_or(-1),
3675 });
3676
3677 let mut __ddb_mas = self.dynamodb_state.write();
3678 let state = __ddb_mas.get_or_create(&self.account_id);
3679 let arn = format!(
3680 "arn:aws:dynamodb:{}:{}:table/{}",
3681 state.region, state.account_id, table_name
3682 );
3683
3684 let stream_arn = if stream_enabled {
3685 Some(format!(
3686 "{}/stream/{}",
3687 arn,
3688 Utc::now().format("%Y-%m-%dT%H:%M:%S.%3f")
3689 ))
3690 } else {
3691 None
3692 };
3693 let stream_arn_attr = stream_arn.clone();
3694
3695 let table = DynamoTable {
3696 name: table_name.to_string(),
3697 arn: arn.clone(),
3698 table_id: Uuid::new_v4().to_string().replace('-', ""),
3699 key_schema,
3700 attribute_definitions,
3701 provisioned_throughput,
3702 items: Vec::new(),
3703 gsi: Vec::new(),
3704 lsi: Vec::new(),
3705 tags: BTreeMap::new(),
3706 created_at: Utc::now(),
3707 status: "ACTIVE".to_string(),
3708 item_count: 0,
3709 size_bytes: 0,
3710 billing_mode,
3711 ttl_attribute: None,
3712 ttl_enabled: false,
3713 resource_policy: None,
3714 pitr_enabled: false,
3715 kinesis_destinations: Vec::new(),
3716 contributor_insights_status: "DISABLED".to_string(),
3717 contributor_insights_counters: BTreeMap::new(),
3718 stream_enabled,
3719 stream_view_type,
3720 stream_arn,
3721 stream_records: Arc::new(RwLock::new(Vec::new())),
3722 sse_type: None,
3723 sse_kms_key_arn: None,
3724 deletion_protection_enabled,
3725 on_demand_throughput,
3726 };
3727
3728 state.tables.insert(table_name.to_string(), table);
3729 let mut result = ProvisionResult::new(arn.clone()).with("Arn", arn);
3730 if let Some(stream_arn_value) = stream_arn_attr {
3731 result = result.with("StreamArn", stream_arn_value);
3732 }
3733 Ok(result)
3734 }
3735
3736 fn delete_dynamodb_table(&self, physical_id: &str) -> Result<(), String> {
3737 let mut __ddb_mas = self.dynamodb_state.write();
3738 let state = __ddb_mas.get_or_create(&self.account_id);
3739 let table_name = state
3741 .tables
3742 .iter()
3743 .find(|(_, t)| t.arn == physical_id)
3744 .map(|(name, _)| name.clone());
3745 if let Some(name) = table_name {
3746 state.tables.remove(&name);
3747 }
3748 Ok(())
3749 }
3750
3751 fn create_log_group(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
3754 let props = &resource.properties;
3755 let log_group_name = props
3756 .get("LogGroupName")
3757 .and_then(|v| v.as_str())
3758 .unwrap_or(&resource.logical_id);
3759
3760 let retention_in_days = props
3761 .get("RetentionInDays")
3762 .and_then(|v| v.as_i64())
3763 .map(|v| v as i32);
3764
3765 let mut logs_accounts = self.logs_state.write();
3766 let state = logs_accounts.get_or_create(&self.account_id);
3767 let arn = format!(
3768 "arn:aws:logs:{}:{}:log-group:{}:*",
3769 state.region, state.account_id, log_group_name
3770 );
3771
3772 let log_group = fakecloud_logs::LogGroup {
3773 name: log_group_name.to_string(),
3774 arn: arn.clone(),
3775 creation_time: Utc::now().timestamp_millis(),
3776 retention_in_days,
3777 kms_key_id: None,
3778 stored_bytes: 0,
3779 log_streams: std::collections::BTreeMap::new(),
3780 tags: std::collections::BTreeMap::new(),
3781 subscription_filters: Vec::new(),
3782 data_protection_policy: None,
3783 index_policies: Vec::new(),
3784 transformer: None,
3785 deletion_protection: false,
3786 log_group_class: Some("STANDARD".to_string()),
3787 };
3788
3789 state
3790 .log_groups
3791 .insert(log_group_name.to_string(), log_group);
3792 Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
3793 }
3794
3795 fn create_lambda_function(
3796 &self,
3797 resource: &ResourceDefinition,
3798 ) -> Result<ProvisionResult, String> {
3799 let props = &resource.properties;
3800 let function_name = props
3801 .get("FunctionName")
3802 .and_then(|v| v.as_str())
3803 .map(|s| s.to_string())
3804 .unwrap_or_else(|| {
3805 format!(
3806 "{}-{}-{}",
3807 self.stack_id
3808 .rsplit('/')
3809 .nth(1)
3810 .unwrap_or(&resource.logical_id),
3811 resource.logical_id,
3812 Uuid::new_v4()
3813 .to_string()
3814 .split('-')
3815 .next()
3816 .unwrap_or("rand")
3817 )
3818 });
3819
3820 let cfg = parse_lambda_function_props(props)?;
3821 let function_arn = format!(
3822 "arn:aws:lambda:{}:{}:function:{}",
3823 self.region, self.account_id, function_name
3824 );
3825
3826 let code_zip = if cfg.code_zip.is_some() {
3832 cfg.code_zip.clone()
3833 } else if let (Some(bucket_name), Some(key)) = (&cfg.s3_bucket, &cfg.s3_key) {
3834 Some(self.read_s3_object_bytes(bucket_name, key).map_err(|e| {
3835 format!("Failed to read Code.S3Bucket={bucket_name} Code.S3Key={key}: {e}")
3836 })?)
3837 } else {
3838 None
3839 };
3840
3841 let (code_sha256, code_size) = match &code_zip {
3845 Some(bytes) => (sha256_b64(bytes), bytes.len() as i64),
3846 None => (String::new(), 0),
3847 };
3848
3849 let layers: Vec<AttachedLayer> = {
3853 let accounts = self.lambda_state.read();
3854 cfg.layers
3855 .iter()
3856 .map(|arn| AttachedLayer {
3857 arn: arn.clone(),
3858 code_size: layer_code_size(&accounts, arn),
3859 })
3860 .collect()
3861 };
3862
3863 let func = fakecloud_lambda::LambdaFunction {
3864 function_name: function_name.clone(),
3865 function_arn: function_arn.clone(),
3866 runtime: cfg.runtime,
3867 role: cfg.role,
3868 handler: cfg.handler,
3869 description: cfg.description,
3870 timeout: cfg.timeout,
3871 memory_size: cfg.memory_size,
3872 code_sha256,
3873 code_size,
3874 version: "$LATEST".to_string(),
3875 last_modified: Utc::now(),
3876 tags: cfg.tags,
3877 environment: cfg.environment,
3878 architectures: cfg.architectures,
3879 package_type: cfg.package_type,
3880 code_zip,
3881 image_uri: cfg.image_uri,
3882 policy: None,
3883 layers,
3884 revision_id: Uuid::new_v4().to_string(),
3885 tracing_mode: cfg.tracing_mode,
3886 kms_key_arn: cfg.kms_key_arn,
3887 ephemeral_storage_size: cfg.ephemeral_storage_size,
3888 vpc_config: cfg.vpc_config,
3889 snap_start: cfg.snap_start,
3890 dead_letter_config_arn: cfg.dead_letter_config_arn,
3891 file_system_configs: cfg.file_system_configs,
3892 logging_config: cfg.logging_config,
3893 image_config: None,
3894 signing_profile_version_arn: None,
3895 signing_job_arn: None,
3896 runtime_version_config: None,
3897 master_arn: None,
3898 state_reason: None,
3899 state_reason_code: None,
3900 last_update_status_reason: None,
3901 last_update_status_reason_code: None,
3902 };
3903
3904 let mut accounts = self.lambda_state.write();
3905 let state = accounts.get_or_create(&self.account_id);
3906 state.functions.insert(function_name.clone(), func);
3907
3908 Ok(ProvisionResult::new(function_name.clone())
3913 .with("Arn", function_arn)
3914 .with("FunctionName", function_name)
3915 .with("Version", "$LATEST"))
3916 }
3917
3918 fn update_lambda_function(
3926 &self,
3927 existing: &StackResource,
3928 resource: &ResourceDefinition,
3929 ) -> Result<ProvisionResult, String> {
3930 let props = &resource.properties;
3931 let function_name = existing.physical_id.clone();
3932 let cfg = parse_lambda_function_props(props)?;
3933
3934 let new_code_zip = if cfg.code_zip.is_some() {
3935 cfg.code_zip.clone()
3936 } else if let (Some(bucket_name), Some(key)) = (&cfg.s3_bucket, &cfg.s3_key) {
3937 Some(self.read_s3_object_bytes(bucket_name, key).map_err(|e| {
3938 format!("Failed to read Code.S3Bucket={bucket_name} Code.S3Key={key}: {e}")
3939 })?)
3940 } else {
3941 None
3942 };
3943
3944 let resolved_layers: Vec<AttachedLayer> = {
3945 let accounts = self.lambda_state.read();
3946 cfg.layers
3947 .iter()
3948 .map(|arn| AttachedLayer {
3949 arn: arn.clone(),
3950 code_size: layer_code_size(&accounts, arn),
3951 })
3952 .collect()
3953 };
3954
3955 let mut accounts = self.lambda_state.write();
3956 let state = accounts.get_or_create(&self.account_id);
3957 let func = state.functions.get_mut(&function_name).ok_or_else(|| {
3958 format!("Cannot update {function_name}: function does not exist in lambda state")
3959 })?;
3960
3961 func.runtime = cfg.runtime;
3962 func.role = cfg.role;
3963 func.handler = cfg.handler;
3964 func.description = cfg.description;
3965 func.timeout = cfg.timeout;
3966 func.memory_size = cfg.memory_size;
3967 func.environment = cfg.environment;
3968 func.architectures = cfg.architectures;
3969 func.package_type = cfg.package_type;
3970 func.tracing_mode = cfg.tracing_mode;
3971 func.kms_key_arn = cfg.kms_key_arn;
3972 func.ephemeral_storage_size = cfg.ephemeral_storage_size;
3973 func.vpc_config = cfg.vpc_config;
3974 func.snap_start = cfg.snap_start;
3975 func.dead_letter_config_arn = cfg.dead_letter_config_arn;
3976 func.file_system_configs = cfg.file_system_configs;
3977 func.logging_config = cfg.logging_config;
3978 func.layers = resolved_layers;
3979 if cfg.image_uri.is_some() {
3980 func.image_uri = cfg.image_uri;
3981 }
3982 if !cfg.tags.is_empty() {
3983 func.tags = cfg.tags;
3984 }
3985 if let Some(bytes) = new_code_zip {
3986 func.code_sha256 = sha256_b64(&bytes);
3987 func.code_size = bytes.len() as i64;
3988 func.code_zip = Some(bytes);
3989 }
3990 func.last_modified = Utc::now();
3991 func.revision_id = Uuid::new_v4().to_string();
3992
3993 let function_arn = func.function_arn.clone();
3994 Ok(ProvisionResult::new(function_name.clone())
3995 .with("Arn", function_arn)
3996 .with("FunctionName", function_name)
3997 .with("Version", "$LATEST"))
3998 }
3999
4000 fn delete_lambda_function(&self, physical_id: &str) -> Result<(), String> {
4001 let mut accounts = self.lambda_state.write();
4002 let state = accounts.default_mut();
4003 state.functions.remove(physical_id);
4004 Ok(())
4005 }
4006
4007 fn read_s3_object_bytes(&self, bucket: &str, key: &str) -> Result<Vec<u8>, String> {
4013 let mut accounts = self.s3_state.write();
4014 let state = accounts.get_or_create(&self.account_id);
4015 let body_ref = {
4016 let b = state
4017 .buckets
4018 .get(bucket)
4019 .ok_or_else(|| format!("S3 bucket {bucket} does not exist"))?;
4020 let object = b
4021 .objects
4022 .get(key)
4023 .ok_or_else(|| format!("S3 object s3://{bucket}/{key} does not exist"))?;
4024 object.body.clone()
4025 };
4026 state
4029 .read_body(&body_ref)
4030 .map(|b| b.to_vec())
4031 .map_err(|e| format!("S3 read failed: {e}"))
4032 }
4033
4034 fn create_lambda_permission(
4035 &self,
4036 resource: &ResourceDefinition,
4037 ) -> Result<ProvisionResult, String> {
4038 let props = &resource.properties;
4039 let function_name = parse_lambda_function_name(
4040 props
4041 .get("FunctionName")
4042 .and_then(|v| v.as_str())
4043 .ok_or_else(|| "FunctionName is required".to_string())?,
4044 );
4045 let statement_id = format!(
4049 "cfn-{}-{}",
4050 resource.logical_id,
4051 &Uuid::new_v4().simple().to_string()[..8]
4052 );
4053 self.append_lambda_permission_statement(&function_name, &statement_id, props)?;
4054
4055 let physical_id = format!("{function_name}|{statement_id}");
4057 Ok(ProvisionResult::new(physical_id).with("Id", statement_id))
4058 }
4059
4060 fn append_lambda_permission_statement(
4066 &self,
4067 function_name: &str,
4068 statement_id: &str,
4069 props: &serde_json::Value,
4070 ) -> Result<String, String> {
4071 let action = props
4072 .get("Action")
4073 .and_then(|v| v.as_str())
4074 .ok_or_else(|| "Action is required".to_string())?
4075 .to_string();
4076 let principal = props
4077 .get("Principal")
4078 .and_then(|v| v.as_str())
4079 .ok_or_else(|| "Principal is required".to_string())?
4080 .to_string();
4081 let source_arn = props
4082 .get("SourceArn")
4083 .and_then(|v| v.as_str())
4084 .map(|s| s.to_string());
4085 let source_account = props
4086 .get("SourceAccount")
4087 .and_then(|v| v.as_str())
4088 .map(|s| s.to_string());
4089 let event_source_token = props
4090 .get("EventSourceToken")
4091 .and_then(|v| v.as_str())
4092 .map(|s| s.to_string());
4093 let function_url_auth_type = props
4094 .get("FunctionUrlAuthType")
4095 .and_then(|v| v.as_str())
4096 .map(|s| s.to_string());
4097 let principal_org_id = props
4098 .get("PrincipalOrgID")
4099 .and_then(|v| v.as_str())
4100 .map(|s| s.to_string());
4101
4102 let mut accounts = self.lambda_state.write();
4103 let state = accounts.get_or_create(&self.account_id);
4104 let func = state.functions.get_mut(function_name).ok_or_else(|| {
4105 format!(
4106 "Function {function_name} does not exist yet — retry once it has been provisioned"
4107 )
4108 })?;
4109
4110 let mut doc: serde_json::Value = func
4111 .policy
4112 .as_deref()
4113 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
4114 .filter(|v| v.is_object())
4115 .unwrap_or_else(|| serde_json::json!({"Version": "2012-10-17", "Statement": []}));
4116 if !doc.get("Statement").map(|s| s.is_array()).unwrap_or(false) {
4117 doc["Statement"] = serde_json::json!([]);
4118 }
4119 let principal_value =
4120 if principal.ends_with(".amazonaws.com") || principal.contains(".amazon") {
4121 serde_json::json!({ "Service": principal })
4122 } else {
4123 serde_json::json!({ "AWS": principal })
4124 };
4125 let mut arn_like = serde_json::Map::new();
4126 let mut string_equals = serde_json::Map::new();
4127 if let Some(src) = source_arn {
4128 arn_like.insert("AWS:SourceArn".to_string(), serde_json::Value::String(src));
4129 }
4130 if let Some(acct) = source_account {
4131 string_equals.insert(
4132 "AWS:SourceAccount".to_string(),
4133 serde_json::Value::String(acct),
4134 );
4135 }
4136 if let Some(token) = event_source_token {
4137 string_equals.insert(
4138 "lambda:EventSourceToken".to_string(),
4139 serde_json::Value::String(token),
4140 );
4141 }
4142 if let Some(auth) = function_url_auth_type {
4143 string_equals.insert(
4144 "lambda:FunctionUrlAuthType".to_string(),
4145 serde_json::Value::String(auth),
4146 );
4147 }
4148 if let Some(org) = principal_org_id {
4149 string_equals.insert(
4150 "aws:PrincipalOrgID".to_string(),
4151 serde_json::Value::String(org),
4152 );
4153 }
4154 let mut conditions = serde_json::Map::new();
4155 if !arn_like.is_empty() {
4156 conditions.insert("ArnLike".to_string(), serde_json::Value::Object(arn_like));
4157 }
4158 if !string_equals.is_empty() {
4159 conditions.insert(
4160 "StringEquals".to_string(),
4161 serde_json::Value::Object(string_equals),
4162 );
4163 }
4164
4165 let mut statement = serde_json::Map::new();
4166 statement.insert(
4167 "Sid".to_string(),
4168 serde_json::Value::String(statement_id.to_string()),
4169 );
4170 statement.insert(
4171 "Effect".to_string(),
4172 serde_json::Value::String("Allow".to_string()),
4173 );
4174 statement.insert("Principal".to_string(), principal_value);
4175 statement.insert("Action".to_string(), serde_json::Value::String(action));
4176 statement.insert(
4177 "Resource".to_string(),
4178 serde_json::Value::String(func.function_arn.clone()),
4179 );
4180 if !conditions.is_empty() {
4181 statement.insert(
4182 "Condition".to_string(),
4183 serde_json::Value::Object(conditions),
4184 );
4185 }
4186 doc["Statement"]
4187 .as_array_mut()
4188 .unwrap()
4189 .push(serde_json::Value::Object(statement));
4190 func.policy = Some(doc.to_string());
4191 Ok(func.function_arn.clone())
4192 }
4193
4194 fn update_lambda_permission(
4199 &self,
4200 existing: &StackResource,
4201 resource: &ResourceDefinition,
4202 ) -> Result<ProvisionResult, String> {
4203 let Some((function_name, statement_id)) = existing.physical_id.split_once('|') else {
4204 return Err(format!(
4205 "Permission physical id `{}` is malformed; expected `{{function}}|{{sid}}`",
4206 existing.physical_id
4207 ));
4208 };
4209 {
4212 let mut accounts = self.lambda_state.write();
4213 let state = accounts.get_or_create(&self.account_id);
4214 if let Some(func) = state.functions.get_mut(function_name) {
4215 if let Some(policy_str) = func.policy.as_deref() {
4216 if let Ok(mut doc) = serde_json::from_str::<serde_json::Value>(policy_str) {
4217 if let Some(arr) = doc.get_mut("Statement").and_then(|v| v.as_array_mut()) {
4218 arr.retain(|s| {
4219 s.get("Sid").and_then(|v| v.as_str()) != Some(statement_id)
4220 });
4221 func.policy = Some(doc.to_string());
4222 }
4223 }
4224 }
4225 }
4226 }
4227 self.append_lambda_permission_statement(function_name, statement_id, &resource.properties)?;
4228 Ok(ProvisionResult::new(existing.physical_id.clone()).with("Id", statement_id.to_string()))
4229 }
4230
4231 fn delete_lambda_permission(&self, physical_id: &str) -> Result<(), String> {
4232 let Some((function_name, sid)) = physical_id.split_once('|') else {
4233 return Ok(());
4234 };
4235 let mut accounts = self.lambda_state.write();
4236 let state = accounts.get_or_create(&self.account_id);
4237 if let Some(func) = state.functions.get_mut(function_name) {
4238 if let Some(policy_str) = func.policy.as_deref() {
4239 if let Ok(mut doc) = serde_json::from_str::<serde_json::Value>(policy_str) {
4240 if let Some(arr) = doc.get_mut("Statement").and_then(|v| v.as_array_mut()) {
4241 arr.retain(|s| s.get("Sid").and_then(|v| v.as_str()) != Some(sid));
4242 func.policy = Some(doc.to_string());
4243 }
4244 }
4245 }
4246 }
4247 Ok(())
4248 }
4249
4250 fn create_lambda_event_source_mapping(
4251 &self,
4252 resource: &ResourceDefinition,
4253 ) -> Result<ProvisionResult, String> {
4254 let props = &resource.properties;
4255 let function_name = parse_lambda_function_name(
4256 props
4257 .get("FunctionName")
4258 .and_then(|v| v.as_str())
4259 .ok_or_else(|| "FunctionName is required".to_string())?,
4260 );
4261 let cfg = parse_lambda_event_source_mapping_props(props)?;
4262
4263 let mut accounts = self.lambda_state.write();
4264 let state = accounts.get_or_create(&self.account_id);
4265 if !state.functions.contains_key(&function_name) {
4266 return Err(format!(
4267 "Function {function_name} does not exist yet — retry once it has been provisioned"
4268 ));
4269 }
4270 let function_arn = format!(
4271 "arn:aws:lambda:{}:{}:function:{}",
4272 self.region, self.account_id, function_name
4273 );
4274 let uuid = Uuid::new_v4().to_string();
4275 let esm = EventSourceMapping {
4276 uuid: uuid.clone(),
4277 function_arn,
4278 event_source_arn: cfg.event_source_arn,
4279 batch_size: cfg.batch_size,
4280 enabled: cfg.enabled,
4281 state: if cfg.enabled {
4282 "Enabled".to_string()
4283 } else {
4284 "Disabled".to_string()
4285 },
4286 last_modified: Utc::now(),
4287 filter_patterns: cfg.filter_patterns,
4288 maximum_batching_window_in_seconds: cfg.maximum_batching_window_in_seconds,
4289 starting_position: cfg.starting_position,
4290 starting_position_timestamp: cfg.starting_position_timestamp,
4291 parallelization_factor: cfg.parallelization_factor,
4292 function_response_types: cfg.function_response_types,
4293 kms_key_arn: cfg.kms_key_arn,
4294 metrics_config: cfg.metrics_config,
4295 destination_config: cfg.destination_config,
4296 maximum_retry_attempts: cfg.maximum_retry_attempts,
4297 maximum_record_age_in_seconds: cfg.maximum_record_age_in_seconds,
4298 bisect_batch_on_function_error: cfg.bisect_batch_on_function_error,
4299 tumbling_window_in_seconds: cfg.tumbling_window_in_seconds,
4300 topics: cfg.topics,
4301 queues: cfg.queues,
4302 };
4303 state.event_source_mappings.insert(uuid.clone(), esm);
4304 Ok(ProvisionResult::new(uuid.clone()).with("Id", uuid))
4305 }
4306
4307 fn update_lambda_event_source_mapping(
4311 &self,
4312 existing: &StackResource,
4313 resource: &ResourceDefinition,
4314 ) -> Result<ProvisionResult, String> {
4315 let cfg = parse_lambda_event_source_mapping_props(&resource.properties)?;
4316 let mut accounts = self.lambda_state.write();
4317 let state = accounts.get_or_create(&self.account_id);
4318 let esm = state
4319 .event_source_mappings
4320 .get_mut(&existing.physical_id)
4321 .ok_or_else(|| {
4322 format!(
4323 "EventSourceMapping {} does not exist in lambda state",
4324 existing.physical_id
4325 )
4326 })?;
4327 esm.batch_size = cfg.batch_size;
4328 esm.enabled = cfg.enabled;
4329 esm.state = if cfg.enabled {
4330 "Enabled".to_string()
4331 } else {
4332 "Disabled".to_string()
4333 };
4334 esm.last_modified = Utc::now();
4335 esm.filter_patterns = cfg.filter_patterns;
4336 esm.maximum_batching_window_in_seconds = cfg.maximum_batching_window_in_seconds;
4337 esm.parallelization_factor = cfg.parallelization_factor;
4338 esm.function_response_types = cfg.function_response_types;
4339 esm.kms_key_arn = cfg.kms_key_arn;
4340 esm.metrics_config = cfg.metrics_config;
4341 esm.destination_config = cfg.destination_config;
4342 esm.maximum_retry_attempts = cfg.maximum_retry_attempts;
4343 esm.maximum_record_age_in_seconds = cfg.maximum_record_age_in_seconds;
4344 esm.bisect_batch_on_function_error = cfg.bisect_batch_on_function_error;
4345 esm.tumbling_window_in_seconds = cfg.tumbling_window_in_seconds;
4346 Ok(ProvisionResult::new(existing.physical_id.clone())
4347 .with("Id", existing.physical_id.clone()))
4348 }
4349
4350 fn delete_lambda_event_source_mapping(&self, physical_id: &str) -> Result<(), String> {
4351 let mut accounts = self.lambda_state.write();
4352 let state = accounts.get_or_create(&self.account_id);
4353 state.event_source_mappings.remove(physical_id);
4354 Ok(())
4355 }
4356
4357 fn create_lambda_layer_version(
4358 &self,
4359 resource: &ResourceDefinition,
4360 ) -> Result<ProvisionResult, String> {
4361 let props = &resource.properties;
4362 let layer_name = props
4363 .get("LayerName")
4364 .and_then(|v| v.as_str())
4365 .unwrap_or(&resource.logical_id)
4366 .to_string();
4367 let description = props
4368 .get("Description")
4369 .and_then(|v| v.as_str())
4370 .unwrap_or("")
4371 .to_string();
4372 let license_info = props
4373 .get("LicenseInfo")
4374 .and_then(|v| v.as_str())
4375 .unwrap_or("")
4376 .to_string();
4377 let compatible_runtimes: Vec<String> = props
4378 .get("CompatibleRuntimes")
4379 .and_then(|v| v.as_array())
4380 .map(|arr| {
4381 arr.iter()
4382 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4383 .collect()
4384 })
4385 .unwrap_or_default();
4386 let compatible_architectures: Vec<String> = props
4387 .get("CompatibleArchitectures")
4388 .and_then(|v| v.as_array())
4389 .map(|arr| {
4390 arr.iter()
4391 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4392 .collect()
4393 })
4394 .unwrap_or_default();
4395 let content = props.get("Content");
4402 let zip_bytes = if let Some(b64) = content
4403 .and_then(|v| v.get("ZipFile"))
4404 .and_then(|v| v.as_str())
4405 {
4406 use base64::Engine;
4407 Some(
4408 base64::engine::general_purpose::STANDARD
4409 .decode(b64)
4410 .map_err(|e| format!("Content.ZipFile is not valid base64: {e}"))?,
4411 )
4412 } else if let (Some(bucket), Some(key)) = (
4413 content
4414 .and_then(|c| c.get("S3Bucket"))
4415 .and_then(|v| v.as_str()),
4416 content
4417 .and_then(|c| c.get("S3Key"))
4418 .and_then(|v| v.as_str()),
4419 ) {
4420 Some(self.read_s3_object_bytes(bucket, key).map_err(|e| {
4421 format!("Failed to read Content.S3Bucket={bucket} Content.S3Key={key}: {e}")
4422 })?)
4423 } else {
4424 None
4425 };
4426
4427 let (code_sha256, code_size) = match zip_bytes.as_deref() {
4428 Some(bytes) => (sha256_b64(bytes), bytes.len() as i64),
4429 None => (String::new(), 0),
4430 };
4431
4432 let mut accounts = self.lambda_state.write();
4433 let state = accounts.get_or_create(&self.account_id);
4434 let layer_arn = format!(
4435 "arn:aws:lambda:{}:{}:layer:{}",
4436 self.region, self.account_id, layer_name
4437 );
4438 let layer = state
4439 .layers
4440 .entry(layer_name.clone())
4441 .or_insert_with(|| Layer {
4442 layer_name: layer_name.clone(),
4443 layer_arn: layer_arn.clone(),
4444 versions: Vec::new(),
4445 });
4446 let next_version = (layer.versions.len() as i64) + 1;
4447 let version_arn = format!("{}:{}", layer.layer_arn, next_version);
4448 layer.versions.push(LayerVersion {
4449 version: next_version,
4450 layer_version_arn: version_arn.clone(),
4451 description: description.clone(),
4452 created_date: Utc::now(),
4453 compatible_runtimes,
4454 license_info,
4455 policy: None,
4456 code_zip: zip_bytes,
4457 code_sha256,
4458 code_size,
4459 compatible_architectures,
4460 });
4461 Ok(ProvisionResult::new(version_arn.clone())
4462 .with("LayerVersionArn", version_arn)
4463 .with("LayerArn", layer_arn))
4464 }
4465
4466 fn delete_lambda_layer_version(&self, physical_id: &str) -> Result<(), String> {
4467 let Some(idx) = physical_id.rfind(':') else {
4469 return Ok(());
4470 };
4471 let (layer_arn, version_part) = physical_id.split_at(idx);
4472 let version_part = &version_part[1..];
4473 let Ok(version) = version_part.parse::<i64>() else {
4474 return Ok(());
4475 };
4476 let layer_name = layer_arn.rsplit(':').next().unwrap_or("").to_string();
4478 let mut accounts = self.lambda_state.write();
4479 let state = accounts.get_or_create(&self.account_id);
4480 if let Some(layer) = state.layers.get_mut(&layer_name) {
4481 layer.versions.retain(|v| v.version != version);
4482 if layer.versions.is_empty() {
4483 state.layers.remove(&layer_name);
4484 }
4485 }
4486 Ok(())
4487 }
4488
4489 fn create_lambda_url(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
4490 let props = &resource.properties;
4491 let function_name = parse_lambda_function_name(
4492 props
4493 .get("TargetFunctionArn")
4494 .and_then(|v| v.as_str())
4495 .ok_or_else(|| "TargetFunctionArn is required".to_string())?,
4496 );
4497 let qualifier = props
4502 .get("Qualifier")
4503 .and_then(|v| v.as_str())
4504 .filter(|s| !s.is_empty())
4505 .map(|s| s.to_string());
4506 let auth_type = props
4507 .get("AuthType")
4508 .and_then(|v| v.as_str())
4509 .unwrap_or("NONE")
4510 .to_string();
4511 let invoke_mode = props
4512 .get("InvokeMode")
4513 .and_then(|v| v.as_str())
4514 .unwrap_or("BUFFERED")
4515 .to_string();
4516 let cors = props.get("Cors").cloned();
4517
4518 let mut accounts = self.lambda_state.write();
4519 let state = accounts.get_or_create(&self.account_id);
4520 if !state.functions.contains_key(&function_name) {
4521 return Err(format!(
4522 "Function {function_name} does not exist yet — retry once it has been provisioned"
4523 ));
4524 }
4525 let function_arn = match &qualifier {
4526 Some(q) => format!(
4527 "arn:aws:lambda:{}:{}:function:{}:{}",
4528 self.region, self.account_id, function_name, q
4529 ),
4530 None => format!(
4531 "arn:aws:lambda:{}:{}:function:{}",
4532 self.region, self.account_id, function_name
4533 ),
4534 };
4535 let function_url = format!("https://{function_name}.lambda-url.{}.on.aws/", self.region);
4536 let now = Utc::now();
4537 let cfg = FunctionUrlConfig {
4538 function_arn: function_arn.clone(),
4539 function_url: function_url.clone(),
4540 auth_type,
4541 cors,
4542 creation_time: now,
4543 last_modified_time: now,
4544 invoke_mode,
4545 };
4546 let key = match &qualifier {
4550 Some(q) => format!("{function_name}:{q}"),
4551 None => function_name.clone(),
4552 };
4553 state.function_url_configs.insert(key.clone(), cfg);
4554
4555 Ok(ProvisionResult::new(key)
4556 .with("FunctionArn", function_arn)
4557 .with("FunctionUrl", function_url))
4558 }
4559
4560 fn update_lambda_url(
4565 &self,
4566 existing: &StackResource,
4567 resource: &ResourceDefinition,
4568 ) -> Result<ProvisionResult, String> {
4569 let props = &resource.properties;
4570 let auth_type = props
4571 .get("AuthType")
4572 .and_then(|v| v.as_str())
4573 .unwrap_or("NONE")
4574 .to_string();
4575 let invoke_mode = props
4576 .get("InvokeMode")
4577 .and_then(|v| v.as_str())
4578 .unwrap_or("BUFFERED")
4579 .to_string();
4580 let cors = props.get("Cors").cloned();
4581
4582 let mut accounts = self.lambda_state.write();
4583 let state = accounts.get_or_create(&self.account_id);
4584 let cfg = state
4585 .function_url_configs
4586 .get_mut(&existing.physical_id)
4587 .ok_or_else(|| {
4588 format!(
4589 "FunctionUrlConfig {} does not exist in lambda state",
4590 existing.physical_id
4591 )
4592 })?;
4593 cfg.auth_type = auth_type;
4594 cfg.invoke_mode = invoke_mode;
4595 cfg.cors = cors;
4596 cfg.last_modified_time = Utc::now();
4597 let function_arn = cfg.function_arn.clone();
4598 let function_url = cfg.function_url.clone();
4599 Ok(ProvisionResult::new(existing.physical_id.clone())
4600 .with("FunctionArn", function_arn)
4601 .with("FunctionUrl", function_url))
4602 }
4603
4604 fn delete_lambda_url(&self, physical_id: &str) -> Result<(), String> {
4605 let mut accounts = self.lambda_state.write();
4606 let state = accounts.get_or_create(&self.account_id);
4607 state.function_url_configs.remove(physical_id);
4608 Ok(())
4609 }
4610
4611 fn create_lambda_alias(
4612 &self,
4613 resource: &ResourceDefinition,
4614 ) -> Result<ProvisionResult, String> {
4615 let props = &resource.properties;
4616 let function_name = parse_lambda_function_name(
4617 props
4618 .get("FunctionName")
4619 .and_then(|v| v.as_str())
4620 .ok_or_else(|| "FunctionName is required".to_string())?,
4621 );
4622 let alias_name = props
4623 .get("Name")
4624 .and_then(|v| v.as_str())
4625 .ok_or_else(|| "Name is required".to_string())?
4626 .to_string();
4627 let function_version = props
4628 .get("FunctionVersion")
4629 .and_then(|v| v.as_str())
4630 .unwrap_or("$LATEST")
4631 .to_string();
4632 let description = props
4633 .get("Description")
4634 .and_then(|v| v.as_str())
4635 .unwrap_or("")
4636 .to_string();
4637 let routing_config = props.get("RoutingConfig").cloned();
4638
4639 let mut accounts = self.lambda_state.write();
4640 let state = accounts.get_or_create(&self.account_id);
4641 if !state.functions.contains_key(&function_name) {
4642 return Err(format!(
4643 "Function {function_name} does not exist yet — retry once it has been provisioned"
4644 ));
4645 }
4646 let alias_arn = format!(
4647 "arn:aws:lambda:{}:{}:function:{}:{}",
4648 self.region, self.account_id, function_name, alias_name
4649 );
4650 let key = format!("{function_name}:{alias_name}");
4651 state.aliases.insert(
4652 key.clone(),
4653 FunctionAlias {
4654 alias_arn: alias_arn.clone(),
4655 name: alias_name,
4656 function_version,
4657 description,
4658 revision_id: Uuid::new_v4().to_string(),
4659 routing_config,
4660 },
4661 );
4662 if let Some(cnt) = props
4668 .get("ProvisionedConcurrencyConfig")
4669 .and_then(|v| v.get("ProvisionedConcurrentExecutions"))
4670 .and_then(|v| v.as_i64())
4671 {
4672 state.provisioned_concurrency.insert(
4673 key.clone(),
4674 fakecloud_lambda::ProvisionedConcurrencyConfig {
4675 requested: cnt,
4676 allocated: cnt,
4677 status: "READY".to_string(),
4678 last_modified: Utc::now(),
4679 },
4680 );
4681 }
4682 Ok(ProvisionResult::new(key).with("AliasArn", alias_arn))
4683 }
4684
4685 fn update_lambda_alias(
4691 &self,
4692 existing: &StackResource,
4693 resource: &ResourceDefinition,
4694 ) -> Result<ProvisionResult, String> {
4695 let props = &resource.properties;
4696 let function_version = props
4697 .get("FunctionVersion")
4698 .and_then(|v| v.as_str())
4699 .unwrap_or("$LATEST")
4700 .to_string();
4701 let description = props
4702 .get("Description")
4703 .and_then(|v| v.as_str())
4704 .unwrap_or("")
4705 .to_string();
4706 let routing_config = props.get("RoutingConfig").cloned();
4707
4708 let mut accounts = self.lambda_state.write();
4709 let state = accounts.get_or_create(&self.account_id);
4710 let alias = state
4711 .aliases
4712 .get_mut(&existing.physical_id)
4713 .ok_or_else(|| {
4714 format!(
4715 "Alias {} does not exist in lambda state",
4716 existing.physical_id
4717 )
4718 })?;
4719 alias.function_version = function_version;
4720 alias.description = description;
4721 alias.routing_config = routing_config;
4722 alias.revision_id = Uuid::new_v4().to_string();
4723 let alias_arn = alias.alias_arn.clone();
4724
4725 match props
4728 .get("ProvisionedConcurrencyConfig")
4729 .and_then(|v| v.get("ProvisionedConcurrentExecutions"))
4730 .and_then(|v| v.as_i64())
4731 {
4732 Some(cnt) => {
4733 state.provisioned_concurrency.insert(
4734 existing.physical_id.clone(),
4735 fakecloud_lambda::ProvisionedConcurrencyConfig {
4736 requested: cnt,
4737 allocated: cnt,
4738 status: "READY".to_string(),
4739 last_modified: Utc::now(),
4740 },
4741 );
4742 }
4743 None => {
4744 state.provisioned_concurrency.remove(&existing.physical_id);
4745 }
4746 }
4747 Ok(ProvisionResult::new(existing.physical_id.clone()).with("AliasArn", alias_arn))
4748 }
4749
4750 fn delete_lambda_alias(&self, physical_id: &str) -> Result<(), String> {
4751 let mut accounts = self.lambda_state.write();
4752 let state = accounts.get_or_create(&self.account_id);
4753 state.aliases.remove(physical_id);
4754 Ok(())
4755 }
4756
4757 fn create_lambda_version(
4758 &self,
4759 resource: &ResourceDefinition,
4760 ) -> Result<ProvisionResult, String> {
4761 let props = &resource.properties;
4762 let function_name = parse_lambda_function_name(
4763 props
4764 .get("FunctionName")
4765 .and_then(|v| v.as_str())
4766 .ok_or_else(|| "FunctionName is required".to_string())?,
4767 );
4768 let description_override = props
4773 .get("Description")
4774 .and_then(|v| v.as_str())
4775 .map(|s| s.to_string());
4776 let expected_sha = props
4777 .get("CodeSha256")
4778 .and_then(|v| v.as_str())
4779 .map(|s| s.to_string());
4780
4781 let mut accounts = self.lambda_state.write();
4782 let state = accounts.get_or_create(&self.account_id);
4783 let func = state
4784 .functions
4785 .get(&function_name)
4786 .ok_or_else(|| format!("Function {function_name} does not exist yet — retry once it has been provisioned"))?
4787 .clone();
4788 if let Some(expected) = &expected_sha {
4789 if !expected.is_empty() && expected != &func.code_sha256 {
4790 return Err(format!(
4791 "PreconditionFailed: CodeSha256 mismatch on {function_name} — expected {expected}, $LATEST is {actual}",
4792 actual = func.code_sha256,
4793 ));
4794 }
4795 }
4796 let versions = state
4797 .function_versions
4798 .entry(function_name.clone())
4799 .or_default();
4800 let next_version = (versions.len() as i64 + 1).to_string();
4801 versions.push(next_version.clone());
4802 let mut snapshot = func.clone();
4805 snapshot.version = next_version.clone();
4806 if let Some(desc) = description_override {
4807 snapshot.description = desc;
4808 }
4809 state
4810 .function_version_snapshots
4811 .entry(function_name.clone())
4812 .or_default()
4813 .insert(next_version.clone(), snapshot);
4814 let version_arn = format!(
4815 "arn:aws:lambda:{}:{}:function:{}:{}",
4816 self.region, self.account_id, function_name, next_version
4817 );
4818 let physical_id = format!("{function_name}:{next_version}");
4819 Ok(ProvisionResult::new(physical_id)
4820 .with("Version", next_version)
4821 .with("FunctionArn", version_arn))
4822 }
4823
4824 fn update_lambda_version(
4830 &self,
4831 existing: &StackResource,
4832 _resource: &ResourceDefinition,
4833 ) -> Result<ProvisionResult, String> {
4834 let mut accounts = self.lambda_state.write();
4835 let state = accounts.get_or_create(&self.account_id);
4836 let Some((function_name, version)) = existing.physical_id.split_once(':') else {
4837 return Err(format!(
4838 "Version physical id `{}` is malformed; expected `{{function}}:{{version}}`",
4839 existing.physical_id
4840 ));
4841 };
4842 let exists = state
4843 .function_version_snapshots
4844 .get(function_name)
4845 .map(|m| m.contains_key(version))
4846 .unwrap_or(false);
4847 if !exists {
4848 return Err(format!(
4849 "Version {version} for function {function_name} no longer exists in lambda state"
4850 ));
4851 }
4852 let version_arn = format!(
4853 "arn:aws:lambda:{}:{}:function:{}:{}",
4854 self.region, self.account_id, function_name, version
4855 );
4856 Ok(ProvisionResult::new(existing.physical_id.clone())
4857 .with("Version", version.to_string())
4858 .with("FunctionArn", version_arn))
4859 }
4860
4861 fn update_lambda_layer_version(
4867 &self,
4868 existing: &StackResource,
4869 _resource: &ResourceDefinition,
4870 ) -> Result<ProvisionResult, String> {
4871 let arn = existing.physical_id.clone();
4872 let layer_arn_only = arn
4874 .rsplit_once(':')
4875 .map(|(prefix, _)| prefix.to_string())
4876 .unwrap_or_else(|| arn.clone());
4877 Ok(ProvisionResult::new(existing.physical_id.clone())
4878 .with("LayerVersionArn", arn)
4879 .with("LayerArn", layer_arn_only))
4880 }
4881
4882 fn delete_lambda_version(&self, physical_id: &str) -> Result<(), String> {
4883 let Some((function_name, version)) = physical_id.split_once(':') else {
4884 return Ok(());
4885 };
4886 let mut accounts = self.lambda_state.write();
4887 let state = accounts.get_or_create(&self.account_id);
4888 if let Some(versions) = state.function_versions.get_mut(function_name) {
4889 versions.retain(|v| v != version);
4890 }
4891 if let Some(snapshots) = state.function_version_snapshots.get_mut(function_name) {
4892 snapshots.remove(version);
4893 }
4894 Ok(())
4895 }
4896
4897 fn create_secrets_manager_secret(
4900 &self,
4901 resource: &ResourceDefinition,
4902 ) -> Result<ProvisionResult, String> {
4903 let props = &resource.properties;
4904 let name = props
4905 .get("Name")
4906 .and_then(|v| v.as_str())
4907 .unwrap_or(&resource.logical_id)
4908 .to_string();
4909 let description = props
4910 .get("Description")
4911 .and_then(|v| v.as_str())
4912 .map(|s| s.to_string());
4913 let kms_key_id = props
4914 .get("KmsKeyId")
4915 .and_then(|v| v.as_str())
4916 .map(|s| s.to_string());
4917
4918 let mut accounts = self.secretsmanager_state.write();
4919 let state = accounts.get_or_create(&self.account_id);
4920 let arn = format!(
4921 "arn:aws:secretsmanager:{}:{}:secret:{}",
4922 state.region, state.account_id, name
4923 );
4924
4925 if state.secrets.contains_key(&arn) {
4926 return Err(format!("Secret {name} already exists"));
4927 }
4928
4929 let now = Utc::now();
4930 let mut versions = BTreeMap::new();
4931 let mut current_version_id: Option<String> = None;
4932 let initial_string: Option<String> =
4933 if let Some(secret_string) = props.get("SecretString").and_then(|v| v.as_str()) {
4934 Some(secret_string.to_string())
4935 } else if let Some(gen) = props.get("GenerateSecretString") {
4936 Some(generate_secret_string_payload(gen)?)
4937 } else {
4938 None
4939 };
4940 if let Some(secret_string) = initial_string {
4941 let version_id = Uuid::new_v4().to_string();
4942 versions.insert(
4943 version_id.clone(),
4944 SecretVersion {
4945 version_id: version_id.clone(),
4946 secret_string: Some(secret_string),
4947 secret_binary: None,
4948 stages: vec!["AWSCURRENT".to_string()],
4949 created_at: now,
4950 },
4951 );
4952 current_version_id = Some(version_id);
4953 }
4954
4955 let mut tags: Vec<(String, String)> = Vec::new();
4956 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
4957 for t in arr {
4958 if let (Some(k), Some(v)) = (
4959 t.get("Key").and_then(|x| x.as_str()),
4960 t.get("Value").and_then(|x| x.as_str()),
4961 ) {
4962 tags.push((k.to_string(), v.to_string()));
4963 }
4964 }
4965 }
4966 let tags_set = !tags.is_empty();
4967
4968 let secret = Secret {
4969 name: name.clone(),
4970 arn: arn.clone(),
4971 description,
4972 kms_key_id,
4973 versions,
4974 current_version_id,
4975 tags,
4976 tags_ever_set: tags_set,
4977 deleted: false,
4978 deletion_date: None,
4979 created_at: now,
4980 last_changed_at: now,
4981 last_accessed_at: None,
4982 rotation_enabled: None,
4983 rotation_lambda_arn: None,
4984 rotation_rules: None,
4985 last_rotated_at: None,
4986 resource_policy: None,
4987 };
4988 state.secrets.insert(arn.clone(), secret);
4989
4990 Ok(ProvisionResult::new(arn.clone())
4991 .with("Id", arn.clone())
4992 .with("Name", name))
4993 }
4994
4995 fn delete_secrets_manager_secret(&self, physical_id: &str) -> Result<(), String> {
4996 let mut accounts = self.secretsmanager_state.write();
4997 let state = accounts.get_or_create(&self.account_id);
4998 state.secrets.remove(physical_id);
4999 Ok(())
5000 }
5001
5002 fn create_kinesis_stream(
5005 &self,
5006 resource: &ResourceDefinition,
5007 ) -> Result<ProvisionResult, String> {
5008 let props = &resource.properties;
5009 let stream_name = props
5010 .get("Name")
5011 .and_then(|v| v.as_str())
5012 .unwrap_or(&resource.logical_id)
5013 .to_string();
5014 let shard_count = props
5015 .get("ShardCount")
5016 .and_then(|v| v.as_i64())
5017 .unwrap_or(1) as i32;
5018 if shard_count <= 0 {
5019 return Err("ShardCount must be greater than zero".to_string());
5020 }
5021 let stream_mode = props
5022 .get("StreamModeDetails")
5023 .and_then(|v| v.get("StreamMode"))
5024 .and_then(|v| v.as_str())
5025 .unwrap_or("PROVISIONED")
5026 .to_string();
5027 let retention_period_hours = props
5028 .get("RetentionPeriodHours")
5029 .and_then(|v| v.as_i64())
5030 .unwrap_or(24) as i32;
5031
5032 let mut accounts = self.kinesis_state.write();
5033 let state = accounts.get_or_create(&self.account_id);
5034 if state.streams.contains_key(&stream_name) {
5035 return Err(format!("Stream {stream_name} already exists"));
5036 }
5037 let stream_arn = format!(
5038 "arn:aws:kinesis:{}:{}:stream/{}",
5039 state.region, state.account_id, stream_name
5040 );
5041 let stream = KinesisStream {
5042 stream_name: stream_name.clone(),
5043 stream_arn: stream_arn.clone(),
5044 stream_status: "ACTIVE".to_string(),
5045 stream_creation_timestamp: Utc::now(),
5046 retention_period_hours,
5047 stream_mode,
5048 encryption_type: "NONE".to_string(),
5049 key_id: None,
5050 shard_count,
5051 open_shard_count: shard_count,
5052 tags: BTreeMap::new(),
5053 shards: build_stream_shards(shard_count),
5054 next_shard_index: shard_count,
5055 enhanced_metrics: Vec::new(),
5056 warm_throughput_mibps: None,
5057 max_record_size_kib: None,
5058 };
5059 state.streams.insert(stream_name.clone(), stream);
5060
5061 Ok(ProvisionResult::new(stream_name).with("Arn", stream_arn))
5062 }
5063
5064 fn delete_kinesis_stream(&self, physical_id: &str) -> Result<(), String> {
5065 let mut accounts = self.kinesis_state.write();
5066 let state = accounts.get_or_create(&self.account_id);
5067 state.streams.remove(physical_id);
5068 Ok(())
5069 }
5070
5071 fn create_kinesis_stream_consumer(
5072 &self,
5073 resource: &ResourceDefinition,
5074 ) -> Result<ProvisionResult, String> {
5075 let props = &resource.properties;
5076 let stream_arn = props
5077 .get("StreamARN")
5078 .and_then(|v| v.as_str())
5079 .ok_or_else(|| "StreamARN is required".to_string())?
5080 .to_string();
5081 let consumer_name = props
5082 .get("ConsumerName")
5083 .and_then(|v| v.as_str())
5084 .ok_or_else(|| "ConsumerName is required".to_string())?
5085 .to_string();
5086
5087 let mut accounts = self.kinesis_state.write();
5088 let state = accounts.get_or_create(&self.account_id);
5089 if state
5090 .consumers
5091 .values()
5092 .any(|c| c.stream_arn == stream_arn && c.consumer_name == consumer_name)
5093 {
5094 return Err(format!(
5095 "Consumer {consumer_name} already exists on stream {stream_arn}"
5096 ));
5097 }
5098 let now = Utc::now();
5099 let consumer_arn = format!(
5100 "{}/consumer/{}:{}",
5101 stream_arn,
5102 consumer_name,
5103 now.timestamp()
5104 );
5105 let consumer = KinesisConsumer {
5106 consumer_name: consumer_name.clone(),
5107 consumer_arn: consumer_arn.clone(),
5108 consumer_status: "ACTIVE".to_string(),
5109 consumer_creation_timestamp: now,
5110 stream_arn: stream_arn.clone(),
5111 };
5112 state.consumers.insert(consumer_arn.clone(), consumer);
5113
5114 Ok(ProvisionResult::new(consumer_arn.clone())
5115 .with("ConsumerARN", consumer_arn)
5116 .with("ConsumerName", consumer_name)
5117 .with("ConsumerStatus", "ACTIVE")
5118 .with("ConsumerCreationTimestamp", now.timestamp().to_string())
5119 .with("StreamARN", stream_arn))
5120 }
5121
5122 fn delete_kinesis_stream_consumer(&self, physical_id: &str) -> Result<(), String> {
5123 let mut accounts = self.kinesis_state.write();
5124 let state = accounts.get_or_create(&self.account_id);
5125 state.consumers.remove(physical_id);
5126 Ok(())
5127 }
5128
5129 fn create_kms_key(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
5132 let input = parse_kms_key_input(&resource.properties);
5133 let (key_id, arn) =
5134 kms_provisioner::provision_key(&self.kms_state, &self.account_id, &input)?;
5135 Ok(ProvisionResult::new(key_id.clone())
5136 .with("Arn", arn)
5137 .with("KeyId", key_id))
5138 }
5139
5140 fn update_kms_key(
5146 &self,
5147 existing: &StackResource,
5148 new_def: &ResourceDefinition,
5149 ) -> Result<ProvisionResult, String> {
5150 let new_input = parse_kms_key_input(&new_def.properties);
5151 let arn = {
5152 let mut accounts = self.kms_state.write();
5153 let state = accounts.get_or_create(&self.account_id);
5154 let key = state
5155 .keys
5156 .get(&existing.physical_id)
5157 .ok_or_else(|| format!("Key '{}' does not exist", existing.physical_id))?;
5158 if key.key_spec != new_input.key_spec
5159 || key.key_usage != new_input.key_usage
5160 || key.origin != new_input.origin
5161 || key.multi_region != new_input.multi_region
5162 {
5163 return Err(
5164 "AWS::KMS::Key updates that change KeySpec, KeyUsage, Origin, or MultiRegion require replacement"
5165 .to_string(),
5166 );
5167 }
5168 key.arn.clone()
5169 };
5170 kms_provisioner::update_key_properties(
5171 &self.kms_state,
5172 &self.account_id,
5173 &existing.physical_id,
5174 kms_provisioner::KeyUpdate {
5175 description: Some(new_input.description.clone()),
5176 enabled: Some(new_input.enabled),
5177 key_rotation_enabled: Some(new_input.key_rotation_enabled),
5178 policy: new_input.policy.clone(),
5179 tags: Some(new_input.tags.clone()),
5180 },
5181 )?;
5182 Ok(ProvisionResult::new(existing.physical_id.clone())
5183 .with("Arn", arn)
5184 .with("KeyId", existing.physical_id.clone()))
5185 }
5186
5187 fn delete_kms_key(&self, physical_id: &str) -> Result<(), String> {
5188 let mut accounts = self.kms_state.write();
5189 let state = accounts.get_or_create(&self.account_id);
5190 state.keys.remove(physical_id);
5191 state.aliases.retain(|_, a| a.target_key_id != physical_id);
5192 Ok(())
5193 }
5194
5195 fn create_kms_replica_key(
5199 &self,
5200 resource: &ResourceDefinition,
5201 ) -> Result<ProvisionResult, String> {
5202 let props = &resource.properties;
5203 let primary_arn = props
5204 .get("PrimaryKeyArn")
5205 .and_then(|v| v.as_str())
5206 .ok_or_else(|| "PrimaryKeyArn is required".to_string())?
5207 .to_string();
5208 let description = props
5209 .get("Description")
5210 .and_then(|v| v.as_str())
5211 .map(str::to_string);
5212 let enabled = props
5213 .get("Enabled")
5214 .and_then(|v| v.as_bool())
5215 .unwrap_or(true);
5216 let policy = parse_key_policy(props);
5217 let tags = parse_tag_list(props);
5218
5219 let (replica_key_id, replica_arn) = kms_provisioner::provision_replica_key(
5220 &self.kms_state,
5221 &self.account_id,
5222 &primary_arn,
5223 description,
5224 enabled,
5225 policy,
5226 tags,
5227 )?;
5228 Ok(ProvisionResult::new(replica_key_id.clone())
5229 .with("KeyId", replica_key_id)
5230 .with("Arn", replica_arn))
5231 }
5232
5233 fn update_kms_replica_key(
5239 &self,
5240 existing: &StackResource,
5241 new_def: &ResourceDefinition,
5242 ) -> Result<ProvisionResult, String> {
5243 let props = &new_def.properties;
5244 let new_primary = props
5245 .get("PrimaryKeyArn")
5246 .and_then(|v| v.as_str())
5247 .ok_or_else(|| "PrimaryKeyArn is required".to_string())?;
5248 {
5251 let mut accounts = self.kms_state.write();
5252 let state = accounts.get_or_create(&self.account_id);
5253 let key = state
5254 .keys
5255 .get(&existing.physical_id)
5256 .ok_or_else(|| format!("ReplicaKey '{}' does not exist", existing.physical_id))?;
5257 if let Some(existing_region) = key.primary_region.as_deref() {
5258 let parts: Vec<&str> = new_primary.split(':').collect();
5259 if parts.len() < 4 || parts[3] != existing_region {
5260 return Err(
5261 "AWS::KMS::ReplicaKey updates that change PrimaryKeyArn require replacement"
5262 .to_string(),
5263 );
5264 }
5265 }
5266 }
5267 let description = props
5268 .get("Description")
5269 .and_then(|v| v.as_str())
5270 .map(str::to_string);
5271 let enabled = props
5272 .get("Enabled")
5273 .and_then(|v| v.as_bool())
5274 .unwrap_or(true);
5275 let policy = parse_key_policy(props);
5276 let tags = parse_tag_list(props);
5277 kms_provisioner::update_key_properties(
5278 &self.kms_state,
5279 &self.account_id,
5280 &existing.physical_id,
5281 kms_provisioner::KeyUpdate {
5282 description,
5283 enabled: Some(enabled),
5284 key_rotation_enabled: None,
5285 policy,
5286 tags: Some(tags),
5287 },
5288 )?;
5289 let arn = {
5290 let mut accounts = self.kms_state.write();
5291 let state = accounts.get_or_create(&self.account_id);
5292 state
5293 .keys
5294 .get(&existing.physical_id)
5295 .map(|k| k.arn.clone())
5296 .unwrap_or_default()
5297 };
5298 Ok(ProvisionResult::new(existing.physical_id.clone())
5299 .with("KeyId", existing.physical_id.clone())
5300 .with("Arn", arn))
5301 }
5302
5303 fn delete_kms_replica_key(&self, physical_id: &str) -> Result<(), String> {
5304 let mut accounts = self.kms_state.write();
5305 let state = accounts.get_or_create(&self.account_id);
5306 state.keys.remove(physical_id);
5307 Ok(())
5308 }
5309
5310 fn create_kms_alias(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
5311 let props = &resource.properties;
5312 let alias_name = props
5313 .get("AliasName")
5314 .and_then(|v| v.as_str())
5315 .ok_or_else(|| "AliasName is required".to_string())?
5316 .to_string();
5317 let target_input = props
5318 .get("TargetKeyId")
5319 .and_then(|v| v.as_str())
5320 .ok_or_else(|| "TargetKeyId is required".to_string())?
5321 .to_string();
5322 let alias = kms_provisioner::provision_alias(
5323 &self.kms_state,
5324 &self.account_id,
5325 &alias_name,
5326 &target_input,
5327 )?;
5328 Ok(ProvisionResult::new(alias))
5329 }
5330
5331 fn update_kms_alias(
5335 &self,
5336 existing: &StackResource,
5337 new_def: &ResourceDefinition,
5338 ) -> Result<ProvisionResult, String> {
5339 let props = &new_def.properties;
5340 let new_alias_name = props
5341 .get("AliasName")
5342 .and_then(|v| v.as_str())
5343 .ok_or_else(|| "AliasName is required".to_string())?;
5344 if new_alias_name != existing.physical_id {
5345 return Err(
5346 "AWS::KMS::Alias updates that change AliasName require replacement".to_string(),
5347 );
5348 }
5349 let target_input = props
5350 .get("TargetKeyId")
5351 .and_then(|v| v.as_str())
5352 .ok_or_else(|| "TargetKeyId is required".to_string())?;
5353 kms_provisioner::update_alias_target(
5354 &self.kms_state,
5355 &self.account_id,
5356 &existing.physical_id,
5357 target_input,
5358 )?;
5359 Ok(ProvisionResult::new(existing.physical_id.clone()))
5360 }
5361
5362 fn delete_kms_alias(&self, physical_id: &str) -> Result<(), String> {
5363 let mut accounts = self.kms_state.write();
5364 let state = accounts.get_or_create(&self.account_id);
5365 state.aliases.remove(physical_id);
5366 Ok(())
5367 }
5368
5369 fn create_ecr_repository(
5372 &self,
5373 resource: &ResourceDefinition,
5374 ) -> Result<ProvisionResult, String> {
5375 let props = &resource.properties;
5376 let repository_name = props
5377 .get("RepositoryName")
5378 .and_then(|v| v.as_str())
5379 .unwrap_or(&resource.logical_id)
5380 .to_string();
5381 let image_tag_mutability = props
5382 .get("ImageTagMutability")
5383 .and_then(|v| v.as_str())
5384 .unwrap_or("MUTABLE")
5385 .to_string();
5386 let scan_on_push = props
5387 .get("ImageScanningConfiguration")
5388 .and_then(|v| v.get("ScanOnPush"))
5389 .and_then(|v| v.as_bool())
5390 .unwrap_or(false);
5391 let encryption_type = props
5392 .get("EncryptionConfiguration")
5393 .and_then(|v| v.get("EncryptionType"))
5394 .and_then(|v| v.as_str())
5395 .unwrap_or("AES256")
5396 .to_string();
5397 let kms_key = props
5398 .get("EncryptionConfiguration")
5399 .and_then(|v| v.get("KmsKey"))
5400 .and_then(|v| v.as_str())
5401 .map(|s| s.to_string());
5402 let policy_text = props
5403 .get("RepositoryPolicyText")
5404 .map(|v| {
5405 if v.is_string() {
5406 v.as_str().unwrap_or("").to_string()
5407 } else {
5408 serde_json::to_string(v).unwrap_or_default()
5409 }
5410 })
5411 .filter(|s| !s.is_empty());
5412 let lifecycle_policy = props
5413 .get("LifecyclePolicy")
5414 .and_then(|v| v.get("LifecyclePolicyText"))
5415 .and_then(|v| v.as_str())
5416 .map(|s| s.to_string());
5417 let mut tags: BTreeMap<String, String> = BTreeMap::new();
5418 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
5419 for t in arr {
5420 if let (Some(k), Some(v)) = (
5421 t.get("Key").and_then(|x| x.as_str()),
5422 t.get("Value").and_then(|x| x.as_str()),
5423 ) {
5424 tags.insert(k.to_string(), v.to_string());
5425 }
5426 }
5427 }
5428
5429 let empty_on_delete = props
5430 .get("EmptyOnDelete")
5431 .and_then(|v| v.as_bool())
5432 .unwrap_or(false);
5433
5434 let mut accounts = self.ecr_state.write();
5435 let state = accounts.get_or_create(&self.account_id);
5436 if state.repositories.contains_key(&repository_name) {
5437 return Err(format!("Repository {repository_name} already exists"));
5438 }
5439 let arn = state.repository_arn(&repository_name);
5440 let registry_id = state.account_id.clone();
5441 let endpoint = format!(
5442 "{}.dkr.ecr.{}.amazonaws.com",
5443 state.account_id, state.region
5444 );
5445 let mut repo = Repository::new(&repository_name, arn.clone(), ®istry_id, &endpoint);
5446 repo.image_tag_mutability = image_tag_mutability;
5447 repo.image_scanning_configuration.scan_on_push = scan_on_push;
5448 repo.encryption_configuration.encryption_type = encryption_type;
5449 repo.encryption_configuration.kms_key = kms_key;
5450 repo.policy = policy_text;
5451 if let Some(policy) = lifecycle_policy.as_ref() {
5454 let prune = fakecloud_ecr::evaluate_lifecycle_policy(&repo, policy);
5455 for digest in &prune {
5456 repo.images.remove(digest);
5457 repo.image_tags.retain(|_, d| d != digest);
5458 }
5459 repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
5460 }
5461 repo.lifecycle_policy = lifecycle_policy;
5462 repo.tags = tags;
5463 let uri = repo.repository_uri.clone();
5464 state.repositories.insert(repository_name.clone(), repo);
5465
5466 Ok(ProvisionResult::new(repository_name)
5467 .with("Arn", arn)
5468 .with("RepositoryUri", uri)
5469 .with("RegistryId", registry_id)
5470 .with("EmptyOnDelete", empty_on_delete.to_string()))
5471 }
5472
5473 fn delete_ecr_repository(&self, physical_id: &str) -> Result<(), String> {
5474 let mut accounts = self.ecr_state.write();
5475 let state = accounts.get_or_create(&self.account_id);
5476 state.repositories.remove(physical_id);
5477 Ok(())
5478 }
5479
5480 fn create_ecr_repository_policy(
5484 &self,
5485 resource: &ResourceDefinition,
5486 ) -> Result<ProvisionResult, String> {
5487 let props = &resource.properties;
5488 let repository_name = props
5489 .get("RepositoryName")
5490 .and_then(|v| v.as_str())
5491 .ok_or_else(|| "RepositoryName is required".to_string())?
5492 .to_string();
5493 let policy_text = props
5494 .get("PolicyText")
5495 .map(|v| {
5496 if v.is_string() {
5497 v.as_str().unwrap_or("").to_string()
5498 } else {
5499 serde_json::to_string(v).unwrap_or_default()
5500 }
5501 })
5502 .ok_or_else(|| "PolicyText is required".to_string())?;
5503 let mut accounts = self.ecr_state.write();
5504 let state = accounts.get_or_create(&self.account_id);
5505 let repo = state
5506 .repositories
5507 .get_mut(&repository_name)
5508 .ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
5509 repo.policy = Some(policy_text);
5510 Ok(ProvisionResult::new(format!(
5511 "{}/{}",
5512 self.account_id, repository_name
5513 )))
5514 }
5515
5516 fn delete_ecr_repository_policy(&self, physical_id: &str) -> Result<(), String> {
5517 let repository_name = physical_id
5518 .split_once('/')
5519 .map(|(_, n)| n.to_string())
5520 .unwrap_or_else(|| physical_id.to_string());
5521 let mut accounts = self.ecr_state.write();
5522 let state = accounts.get_or_create(&self.account_id);
5523 if let Some(repo) = state.repositories.get_mut(&repository_name) {
5524 repo.policy = None;
5525 }
5526 Ok(())
5527 }
5528
5529 fn create_ecr_registry_policy(
5532 &self,
5533 resource: &ResourceDefinition,
5534 ) -> Result<ProvisionResult, String> {
5535 let props = &resource.properties;
5536 let policy_text = props
5537 .get("PolicyText")
5538 .map(|v| {
5539 if v.is_string() {
5540 v.as_str().unwrap_or("").to_string()
5541 } else {
5542 serde_json::to_string(v).unwrap_or_default()
5543 }
5544 })
5545 .ok_or_else(|| "PolicyText is required".to_string())?;
5546 let mut accounts = self.ecr_state.write();
5547 let state = accounts.get_or_create(&self.account_id);
5548 state.registry_policy = Some(policy_text);
5549 Ok(ProvisionResult::new(self.account_id.clone())
5550 .with("RegistryId", self.account_id.clone()))
5551 }
5552
5553 fn delete_ecr_registry_policy(&self) -> Result<(), String> {
5554 let mut accounts = self.ecr_state.write();
5555 let state = accounts.get_or_create(&self.account_id);
5556 state.registry_policy = None;
5557 Ok(())
5558 }
5559
5560 fn create_ecr_replication_configuration(
5562 &self,
5563 resource: &ResourceDefinition,
5564 ) -> Result<ProvisionResult, String> {
5565 use fakecloud_ecr::state::{
5566 ReplicationConfiguration, ReplicationDestination, ReplicationRule, RepositoryFilter,
5567 };
5568 let cfg = resource
5569 .properties
5570 .get("ReplicationConfiguration")
5571 .ok_or_else(|| "ReplicationConfiguration is required".to_string())?;
5572 let rules: Vec<ReplicationRule> = cfg
5573 .get("Rules")
5574 .and_then(|v| v.as_array())
5575 .map(|arr| {
5576 arr.iter()
5577 .map(|r| {
5578 let destinations: Vec<ReplicationDestination> = r
5579 .get("Destinations")
5580 .and_then(|v| v.as_array())
5581 .map(|d| {
5582 d.iter()
5583 .map(|dest| ReplicationDestination {
5584 region: dest
5585 .get("Region")
5586 .and_then(|v| v.as_str())
5587 .unwrap_or_default()
5588 .to_string(),
5589 registry_id: dest
5590 .get("RegistryId")
5591 .and_then(|v| v.as_str())
5592 .unwrap_or_default()
5593 .to_string(),
5594 })
5595 .collect()
5596 })
5597 .unwrap_or_default();
5598 let repository_filters: Vec<RepositoryFilter> = r
5599 .get("RepositoryFilters")
5600 .and_then(|v| v.as_array())
5601 .map(|f| {
5602 f.iter()
5603 .map(|f| RepositoryFilter {
5604 filter: f
5605 .get("Filter")
5606 .and_then(|v| v.as_str())
5607 .unwrap_or_default()
5608 .to_string(),
5609 filter_type: f
5610 .get("FilterType")
5611 .and_then(|v| v.as_str())
5612 .unwrap_or_default()
5613 .to_string(),
5614 })
5615 .collect()
5616 })
5617 .unwrap_or_default();
5618 ReplicationRule {
5619 destinations,
5620 repository_filters,
5621 }
5622 })
5623 .collect()
5624 })
5625 .unwrap_or_default();
5626 let mut accounts = self.ecr_state.write();
5627 let state = accounts.get_or_create(&self.account_id);
5628 state.replication_configuration = Some(ReplicationConfiguration { rules });
5629 Ok(ProvisionResult::new(self.account_id.clone()))
5630 }
5631
5632 fn delete_ecr_replication_configuration(&self) -> Result<(), String> {
5633 let mut accounts = self.ecr_state.write();
5634 let state = accounts.get_or_create(&self.account_id);
5635 state.replication_configuration = None;
5636 Ok(())
5637 }
5638
5639 fn create_ecr_pull_through_cache_rule(
5640 &self,
5641 resource: &ResourceDefinition,
5642 ) -> Result<ProvisionResult, String> {
5643 use fakecloud_ecr::state::PullThroughCacheRule;
5644 let props = &resource.properties;
5645 let prefix = props
5646 .get("EcrRepositoryPrefix")
5647 .and_then(|v| v.as_str())
5648 .ok_or_else(|| "EcrRepositoryPrefix is required".to_string())?
5649 .to_string();
5650 let upstream_url = props
5651 .get("UpstreamRegistryUrl")
5652 .and_then(|v| v.as_str())
5653 .ok_or_else(|| "UpstreamRegistryUrl is required".to_string())?
5654 .to_string();
5655 let upstream_registry = props
5656 .get("UpstreamRegistry")
5657 .and_then(|v| v.as_str())
5658 .map(|s| s.to_string());
5659 let credential_arn = props
5660 .get("CredentialArn")
5661 .and_then(|v| v.as_str())
5662 .map(|s| s.to_string());
5663 let custom_role_arn = props
5664 .get("CustomRoleArn")
5665 .and_then(|v| v.as_str())
5666 .map(|s| s.to_string());
5667 let now = Utc::now();
5668 let rule = PullThroughCacheRule {
5669 ecr_repository_prefix: prefix.clone(),
5670 upstream_registry_url: upstream_url,
5671 upstream_registry,
5672 credential_arn,
5673 created_at: now,
5674 updated_at: now,
5675 custom_role_arn,
5676 };
5677 let mut accounts = self.ecr_state.write();
5678 let state = accounts.get_or_create(&self.account_id);
5679 state.pull_through_cache_rules.insert(prefix.clone(), rule);
5680 Ok(ProvisionResult::new(prefix))
5681 }
5682
5683 fn delete_ecr_pull_through_cache_rule(&self, physical_id: &str) -> Result<(), String> {
5684 let mut accounts = self.ecr_state.write();
5685 let state = accounts.get_or_create(&self.account_id);
5686 state.pull_through_cache_rules.remove(physical_id);
5687 Ok(())
5688 }
5689
5690 fn create_ecr_lifecycle_policy(
5696 &self,
5697 resource: &ResourceDefinition,
5698 ) -> Result<ProvisionResult, String> {
5699 let props = &resource.properties;
5700 let repository_name = props
5701 .get("RepositoryName")
5702 .and_then(|v| v.as_str())
5703 .ok_or_else(|| "RepositoryName is required".to_string())?
5704 .to_string();
5705 let policy_text = props
5706 .get("LifecyclePolicyText")
5707 .map(|v| {
5708 if v.is_string() {
5709 v.as_str().unwrap_or("").to_string()
5710 } else {
5711 serde_json::to_string(v).unwrap_or_default()
5712 }
5713 })
5714 .ok_or_else(|| "LifecyclePolicyText is required".to_string())?;
5715 let _registry_id = props
5718 .get("RegistryId")
5719 .and_then(|v| v.as_str())
5720 .map(|s| s.to_string());
5721 let mut accounts = self.ecr_state.write();
5722 let state = accounts.get_or_create(&self.account_id);
5723 let repo = state
5724 .repositories
5725 .get_mut(&repository_name)
5726 .ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
5727 let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, &policy_text);
5730 for digest in &prune {
5731 repo.images.remove(digest);
5732 repo.image_tags.retain(|_, d| d != digest);
5733 }
5734 repo.lifecycle_policy = Some(policy_text);
5735 repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
5736 let registry_id = repo.registry_id.clone();
5737 Ok(
5738 ProvisionResult::new(format!("{}/{}", self.account_id, repository_name))
5739 .with("RepositoryName", repository_name)
5740 .with("RegistryId", registry_id),
5741 )
5742 }
5743
5744 fn delete_ecr_lifecycle_policy(&self, physical_id: &str) -> Result<(), String> {
5745 let repository_name = physical_id
5746 .split_once('/')
5747 .map(|(_, n)| n.to_string())
5748 .unwrap_or_else(|| physical_id.to_string());
5749 let mut accounts = self.ecr_state.write();
5750 let state = accounts.get_or_create(&self.account_id);
5751 if let Some(repo) = state.repositories.get_mut(&repository_name) {
5752 repo.lifecycle_policy = None;
5753 repo.lifecycle_policy_last_evaluated_at = None;
5754 }
5755 Ok(())
5756 }
5757
5758 fn create_ecr_registry_scanning_configuration(
5762 &self,
5763 resource: &ResourceDefinition,
5764 ) -> Result<ProvisionResult, String> {
5765 use fakecloud_ecr::state::{
5766 RegistryScanningConfiguration, RegistryScanningRule, RepositoryFilter,
5767 };
5768 let props = &resource.properties;
5769 let scan_type = props
5770 .get("ScanType")
5771 .and_then(|v| v.as_str())
5772 .unwrap_or("BASIC")
5773 .to_string();
5774 let rules: Vec<RegistryScanningRule> = props
5775 .get("Rules")
5776 .and_then(|v| v.as_array())
5777 .map(|arr| {
5778 arr.iter()
5779 .map(|r| {
5780 let scan_frequency = r
5781 .get("ScanFrequency")
5782 .and_then(|v| v.as_str())
5783 .unwrap_or("MANUAL")
5784 .to_string();
5785 let repository_filters: Vec<RepositoryFilter> = r
5786 .get("RepositoryFilters")
5787 .and_then(|v| v.as_array())
5788 .map(|f| {
5789 f.iter()
5790 .map(|f| RepositoryFilter {
5791 filter: f
5792 .get("Filter")
5793 .and_then(|v| v.as_str())
5794 .unwrap_or_default()
5795 .to_string(),
5796 filter_type: f
5797 .get("FilterType")
5798 .and_then(|v| v.as_str())
5799 .unwrap_or_default()
5800 .to_string(),
5801 })
5802 .collect()
5803 })
5804 .unwrap_or_default();
5805 RegistryScanningRule {
5806 scan_frequency,
5807 repository_filters,
5808 }
5809 })
5810 .collect()
5811 })
5812 .unwrap_or_default();
5813 let mut accounts = self.ecr_state.write();
5814 let state = accounts.get_or_create(&self.account_id);
5815 state.registry_scanning_configuration = RegistryScanningConfiguration { scan_type, rules };
5816 Ok(ProvisionResult::new(self.account_id.clone()))
5817 }
5818
5819 fn delete_ecr_registry_scanning_configuration(&self) -> Result<(), String> {
5820 use fakecloud_ecr::state::RegistryScanningConfiguration;
5821 let mut accounts = self.ecr_state.write();
5822 let state = accounts.get_or_create(&self.account_id);
5823 state.registry_scanning_configuration = RegistryScanningConfiguration::default();
5825 Ok(())
5826 }
5827
5828 fn update_ecr_repository(
5834 &self,
5835 existing: &StackResource,
5836 resource: &ResourceDefinition,
5837 ) -> Result<ProvisionResult, String> {
5838 let props = &resource.properties;
5839 let repository_name = existing.physical_id.clone();
5840 let mut accounts = self.ecr_state.write();
5841 let state = accounts.get_or_create(&self.account_id);
5842 let repo = state
5843 .repositories
5844 .get_mut(&repository_name)
5845 .ok_or_else(|| format!("Repository {repository_name} no longer exists"))?;
5846 if let Some(s) = props.get("ImageTagMutability").and_then(|v| v.as_str()) {
5847 repo.image_tag_mutability = s.to_string();
5848 }
5849 if let Some(b) = props
5850 .get("ImageScanningConfiguration")
5851 .and_then(|v| v.get("ScanOnPush"))
5852 .and_then(|v| v.as_bool())
5853 {
5854 repo.image_scanning_configuration.scan_on_push = b;
5855 }
5856 if let Some(cfg) = props.get("EncryptionConfiguration") {
5857 if let Some(s) = cfg.get("EncryptionType").and_then(|v| v.as_str()) {
5858 repo.encryption_configuration.encryption_type = s.to_string();
5859 }
5860 if let Some(s) = cfg.get("KmsKey").and_then(|v| v.as_str()) {
5861 repo.encryption_configuration.kms_key = Some(s.to_string());
5862 }
5863 }
5864 if let Some(v) = props.get("RepositoryPolicyText") {
5865 let text = if v.is_string() {
5866 v.as_str().unwrap_or("").to_string()
5867 } else {
5868 serde_json::to_string(v).unwrap_or_default()
5869 };
5870 repo.policy = if text.is_empty() { None } else { Some(text) };
5871 }
5872 if let Some(text) = props
5873 .get("LifecyclePolicy")
5874 .and_then(|v| v.get("LifecyclePolicyText"))
5875 .and_then(|v| v.as_str())
5876 {
5877 let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, text);
5878 for digest in &prune {
5879 repo.images.remove(digest);
5880 repo.image_tags.retain(|_, d| d != digest);
5881 }
5882 repo.lifecycle_policy = Some(text.to_string());
5883 repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
5884 }
5885 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
5886 let mut tags: BTreeMap<String, String> = BTreeMap::new();
5887 for t in arr {
5888 if let (Some(k), Some(v)) = (
5889 t.get("Key").and_then(|x| x.as_str()),
5890 t.get("Value").and_then(|x| x.as_str()),
5891 ) {
5892 tags.insert(k.to_string(), v.to_string());
5893 }
5894 }
5895 repo.tags = tags;
5896 }
5897 let arn = repo.repository_arn.clone();
5898 let uri = repo.repository_uri.clone();
5899 let registry_id = repo.registry_id.clone();
5900 Ok(ProvisionResult::new(repository_name)
5901 .with("Arn", arn)
5902 .with("RepositoryUri", uri)
5903 .with("RegistryId", registry_id))
5904 }
5905
5906 fn update_ecr_repository_policy(
5907 &self,
5908 existing: &StackResource,
5909 resource: &ResourceDefinition,
5910 ) -> Result<ProvisionResult, String> {
5911 let props = &resource.properties;
5912 let physical_id = existing.physical_id.clone();
5913 let repository_name = physical_id
5914 .split_once('/')
5915 .map(|(_, n)| n.to_string())
5916 .unwrap_or_else(|| physical_id.clone());
5917 let policy_text = props
5918 .get("PolicyText")
5919 .map(|v| {
5920 if v.is_string() {
5921 v.as_str().unwrap_or("").to_string()
5922 } else {
5923 serde_json::to_string(v).unwrap_or_default()
5924 }
5925 })
5926 .ok_or_else(|| "PolicyText is required".to_string())?;
5927 let mut accounts = self.ecr_state.write();
5928 let state = accounts.get_or_create(&self.account_id);
5929 let repo = state
5930 .repositories
5931 .get_mut(&repository_name)
5932 .ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
5933 repo.policy = Some(policy_text);
5934 Ok(ProvisionResult::new(physical_id))
5935 }
5936
5937 fn update_ecr_lifecycle_policy(
5938 &self,
5939 existing: &StackResource,
5940 resource: &ResourceDefinition,
5941 ) -> Result<ProvisionResult, String> {
5942 let props = &resource.properties;
5943 let physical_id = existing.physical_id.clone();
5944 let repository_name = physical_id
5945 .split_once('/')
5946 .map(|(_, n)| n.to_string())
5947 .unwrap_or_else(|| physical_id.clone());
5948 let policy_text = props
5949 .get("LifecyclePolicyText")
5950 .map(|v| {
5951 if v.is_string() {
5952 v.as_str().unwrap_or("").to_string()
5953 } else {
5954 serde_json::to_string(v).unwrap_or_default()
5955 }
5956 })
5957 .ok_or_else(|| "LifecyclePolicyText is required".to_string())?;
5958 let mut accounts = self.ecr_state.write();
5959 let state = accounts.get_or_create(&self.account_id);
5960 let repo = state
5961 .repositories
5962 .get_mut(&repository_name)
5963 .ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
5964 let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, &policy_text);
5965 for digest in &prune {
5966 repo.images.remove(digest);
5967 repo.image_tags.retain(|_, d| d != digest);
5968 }
5969 repo.lifecycle_policy = Some(policy_text);
5970 repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
5971 let registry_id = repo.registry_id.clone();
5972 Ok(ProvisionResult::new(physical_id)
5973 .with("RepositoryName", repository_name)
5974 .with("RegistryId", registry_id))
5975 }
5976
5977 fn update_ecr_registry_policy(
5978 &self,
5979 existing: &StackResource,
5980 resource: &ResourceDefinition,
5981 ) -> Result<ProvisionResult, String> {
5982 let props = &resource.properties;
5983 let policy_text = props
5984 .get("PolicyText")
5985 .map(|v| {
5986 if v.is_string() {
5987 v.as_str().unwrap_or("").to_string()
5988 } else {
5989 serde_json::to_string(v).unwrap_or_default()
5990 }
5991 })
5992 .ok_or_else(|| "PolicyText is required".to_string())?;
5993 let mut accounts = self.ecr_state.write();
5994 let state = accounts.get_or_create(&self.account_id);
5995 state.registry_policy = Some(policy_text);
5996 Ok(ProvisionResult::new(existing.physical_id.clone())
5997 .with("RegistryId", self.account_id.clone()))
5998 }
5999
6000 fn update_ecr_replication_configuration(
6001 &self,
6002 existing: &StackResource,
6003 resource: &ResourceDefinition,
6004 ) -> Result<ProvisionResult, String> {
6005 let result = self.create_ecr_replication_configuration(resource)?;
6007 Ok(ProvisionResult::new(existing.physical_id.clone()).merge_attributes(result.attributes))
6008 }
6009
6010 fn update_ecr_registry_scanning_configuration(
6011 &self,
6012 existing: &StackResource,
6013 resource: &ResourceDefinition,
6014 ) -> Result<ProvisionResult, String> {
6015 let result = self.create_ecr_registry_scanning_configuration(resource)?;
6016 Ok(ProvisionResult::new(existing.physical_id.clone()).merge_attributes(result.attributes))
6017 }
6018
6019 fn update_ecr_pull_through_cache_rule(
6020 &self,
6021 existing: &StackResource,
6022 resource: &ResourceDefinition,
6023 ) -> Result<ProvisionResult, String> {
6024 let props = &resource.properties;
6025 let prefix = existing.physical_id.clone();
6026 let mut accounts = self.ecr_state.write();
6027 let state = accounts.get_or_create(&self.account_id);
6028 let rule = state
6029 .pull_through_cache_rules
6030 .get_mut(&prefix)
6031 .ok_or_else(|| format!("PullThroughCacheRule {prefix} no longer exists"))?;
6032 if let Some(s) = props.get("UpstreamRegistryUrl").and_then(|v| v.as_str()) {
6033 rule.upstream_registry_url = s.to_string();
6034 }
6035 if let Some(s) = props.get("UpstreamRegistry").and_then(|v| v.as_str()) {
6036 rule.upstream_registry = Some(s.to_string());
6037 }
6038 if let Some(s) = props.get("CredentialArn").and_then(|v| v.as_str()) {
6039 rule.credential_arn = Some(s.to_string());
6040 }
6041 if let Some(s) = props.get("CustomRoleArn").and_then(|v| v.as_str()) {
6042 rule.custom_role_arn = Some(s.to_string());
6043 }
6044 rule.updated_at = Utc::now();
6045 Ok(ProvisionResult::new(prefix))
6046 }
6047
6048 fn get_att_ecr_repository(&self, physical_id: &str, attribute: &str) -> Option<String> {
6049 let mut accounts = self.ecr_state.write();
6050 let state = accounts.get_or_create(&self.account_id);
6051 let repo = state.repositories.get(physical_id)?;
6052 match attribute {
6053 "Arn" => Some(repo.repository_arn.clone()),
6054 "RepositoryUri" => Some(repo.repository_uri.clone()),
6055 "RegistryId" => Some(repo.registry_id.clone()),
6056 _ => None,
6057 }
6058 }
6059
6060 fn create_cloudwatch_alarm(
6063 &self,
6064 resource: &ResourceDefinition,
6065 ) -> Result<ProvisionResult, String> {
6066 let props = &resource.properties;
6067 let alarm_name = props
6068 .get("AlarmName")
6069 .and_then(|v| v.as_str())
6070 .unwrap_or(&resource.logical_id)
6071 .to_string();
6072 let alarm_description = props
6073 .get("AlarmDescription")
6074 .and_then(|v| v.as_str())
6075 .map(|s| s.to_string());
6076 let actions_enabled = props
6077 .get("ActionsEnabled")
6078 .and_then(|v| v.as_bool())
6079 .unwrap_or(true);
6080 let str_array = |key: &str| -> Vec<String> {
6081 props
6082 .get(key)
6083 .and_then(|v| v.as_array())
6084 .map(|arr| {
6085 arr.iter()
6086 .filter_map(|x| x.as_str().map(|s| s.to_string()))
6087 .collect()
6088 })
6089 .unwrap_or_default()
6090 };
6091 let alarm_actions = str_array("AlarmActions");
6092 let ok_actions = str_array("OKActions");
6093 let insufficient_data_actions = str_array("InsufficientDataActions");
6094
6095 let metric_name = props
6096 .get("MetricName")
6097 .and_then(|v| v.as_str())
6098 .map(|s| s.to_string());
6099 let namespace = props
6100 .get("Namespace")
6101 .and_then(|v| v.as_str())
6102 .map(|s| s.to_string());
6103 let statistic = props
6104 .get("Statistic")
6105 .and_then(|v| v.as_str())
6106 .map(|s| s.to_string());
6107 let extended_statistic = props
6108 .get("ExtendedStatistic")
6109 .and_then(|v| v.as_str())
6110 .map(|s| s.to_string());
6111 let unit = props
6112 .get("Unit")
6113 .and_then(|v| v.as_str())
6114 .map(|s| s.to_string());
6115 let period = props.get("Period").and_then(|v| v.as_i64());
6116 let evaluation_periods = props
6117 .get("EvaluationPeriods")
6118 .and_then(|v| v.as_i64())
6119 .unwrap_or(1);
6120 let datapoints_to_alarm = props.get("DatapointsToAlarm").and_then(|v| v.as_i64());
6121 let threshold = props.get("Threshold").and_then(|v| v.as_f64());
6122 let comparison_operator = props
6123 .get("ComparisonOperator")
6124 .and_then(|v| v.as_str())
6125 .unwrap_or("GreaterThanThreshold")
6126 .to_string();
6127 let treat_missing_data = props
6128 .get("TreatMissingData")
6129 .and_then(|v| v.as_str())
6130 .map(|s| s.to_string());
6131 let evaluate_low_sample_count_percentile = props
6132 .get("EvaluateLowSampleCountPercentile")
6133 .and_then(|v| v.as_str())
6134 .map(|s| s.to_string());
6135
6136 let mut dimensions: BTreeMap<String, String> = BTreeMap::new();
6137 if let Some(arr) = props.get("Dimensions").and_then(|v| v.as_array()) {
6138 for d in arr {
6139 if let (Some(k), Some(v)) = (
6140 d.get("Name").and_then(|x| x.as_str()),
6141 d.get("Value").and_then(|x| x.as_str()),
6142 ) {
6143 dimensions.insert(k.to_string(), v.to_string());
6144 }
6145 }
6146 }
6147
6148 let mut accounts = self.cloudwatch_state.write();
6149 let state = accounts.get_or_create(&self.account_id);
6150 let alarm_arn = format!(
6151 "arn:aws:cloudwatch:{}:{}:alarm:{}",
6152 self.region, self.account_id, alarm_name
6153 );
6154 let now = Utc::now();
6155 let alarm = MetricAlarm {
6156 alarm_name: alarm_name.clone(),
6157 alarm_arn: alarm_arn.clone(),
6158 alarm_description,
6159 actions_enabled,
6160 ok_actions,
6161 alarm_actions,
6162 insufficient_data_actions,
6163 state_value: AlarmState::InsufficientData,
6164 state_reason: "Unchecked: Initial alarm creation".to_string(),
6165 state_updated_timestamp: now,
6166 metric_name,
6167 namespace,
6168 statistic,
6169 extended_statistic,
6170 dimensions,
6171 period,
6172 unit,
6173 evaluation_periods,
6174 datapoints_to_alarm,
6175 threshold,
6176 comparison_operator,
6177 treat_missing_data,
6178 evaluate_low_sample_count_percentile,
6179 configuration_updated_timestamp: now,
6180 alarm_configuration_updated_timestamp: now,
6181 };
6182 let region_alarms = state.alarms_in_mut(&self.region);
6183 if region_alarms.contains_key(&alarm_name) {
6184 return Err(format!("Alarm {alarm_name} already exists"));
6185 }
6186 region_alarms.insert(alarm_name.clone(), alarm);
6187
6188 Ok(ProvisionResult::new(alarm_name).with("Arn", alarm_arn))
6189 }
6190
6191 fn delete_cloudwatch_alarm(&self, physical_id: &str) -> Result<(), String> {
6192 let mut accounts = self.cloudwatch_state.write();
6193 let state = accounts.get_or_create(&self.account_id);
6194 state.alarms_in_mut(&self.region).remove(physical_id);
6195 Ok(())
6196 }
6197
6198 fn create_cloudwatch_dashboard(
6199 &self,
6200 resource: &ResourceDefinition,
6201 ) -> Result<ProvisionResult, String> {
6202 let props = &resource.properties;
6203 let dashboard_name = props
6204 .get("DashboardName")
6205 .and_then(|v| v.as_str())
6206 .map(String::from)
6207 .unwrap_or_else(|| {
6208 let suffix = Uuid::new_v4().simple().to_string();
6209 format!("{}-{}", resource.logical_id, &suffix[..8])
6210 });
6211 let body = props
6213 .get("DashboardBody")
6214 .ok_or("DashboardBody is required")?;
6215 let body_str = if let Some(s) = body.as_str() {
6216 s.to_string()
6217 } else {
6218 serde_json::to_string(body).map_err(|e| format!("invalid DashboardBody: {e}"))?
6219 };
6220 serde_json::from_str::<serde_json::Value>(&body_str)
6222 .map_err(|e| format!("DashboardBody must be valid JSON: {e}"))?;
6223
6224 let arn = format!(
6225 "arn:aws:cloudwatch::{}:dashboard/{dashboard_name}",
6226 self.account_id
6227 );
6228 let dashboard = Dashboard {
6229 name: dashboard_name.clone(),
6230 arn: arn.clone(),
6231 size_bytes: body_str.len() as i64,
6232 body: body_str,
6233 last_modified: Utc::now(),
6234 };
6235
6236 let mut accounts = self.cloudwatch_state.write();
6237 let state = accounts.get_or_create(&self.account_id);
6238 state.dashboards.insert(dashboard_name.clone(), dashboard);
6239
6240 Ok(ProvisionResult::new(dashboard_name).with("Arn", arn))
6241 }
6242
6243 fn delete_cloudwatch_dashboard(&self, physical_id: &str) -> Result<(), String> {
6244 let mut accounts = self.cloudwatch_state.write();
6245 let state = accounts.get_or_create(&self.account_id);
6246 state.dashboards.remove(physical_id);
6247 Ok(())
6248 }
6249
6250 fn update_cloudwatch_alarm(
6251 &self,
6252 existing: &StackResource,
6253 new_def: &ResourceDefinition,
6254 ) -> Result<ProvisionResult, String> {
6255 let props = &new_def.properties;
6256 let new_alarm_name = props
6258 .get("AlarmName")
6259 .and_then(|v| v.as_str())
6260 .unwrap_or(&new_def.logical_id);
6261 if new_alarm_name != existing.physical_id {
6262 return Err(
6263 "AWS::CloudWatch::Alarm updates that change AlarmName require replacement"
6264 .to_string(),
6265 );
6266 }
6267
6268 let str_array = |key: &str| -> Vec<String> {
6269 props
6270 .get(key)
6271 .and_then(|v| v.as_array())
6272 .map(|arr| {
6273 arr.iter()
6274 .filter_map(|x| x.as_str().map(|s| s.to_string()))
6275 .collect()
6276 })
6277 .unwrap_or_default()
6278 };
6279 let alarm_description = props
6280 .get("AlarmDescription")
6281 .and_then(|v| v.as_str())
6282 .map(|s| s.to_string());
6283 let actions_enabled = props
6284 .get("ActionsEnabled")
6285 .and_then(|v| v.as_bool())
6286 .unwrap_or(true);
6287 let alarm_actions = str_array("AlarmActions");
6288 let ok_actions = str_array("OKActions");
6289 let insufficient_data_actions = str_array("InsufficientDataActions");
6290 let metric_name = props
6291 .get("MetricName")
6292 .and_then(|v| v.as_str())
6293 .map(|s| s.to_string());
6294 let namespace = props
6295 .get("Namespace")
6296 .and_then(|v| v.as_str())
6297 .map(|s| s.to_string());
6298 let statistic = props
6299 .get("Statistic")
6300 .and_then(|v| v.as_str())
6301 .map(|s| s.to_string());
6302 let extended_statistic = props
6303 .get("ExtendedStatistic")
6304 .and_then(|v| v.as_str())
6305 .map(|s| s.to_string());
6306 let unit = props
6307 .get("Unit")
6308 .and_then(|v| v.as_str())
6309 .map(|s| s.to_string());
6310 let period = props.get("Period").and_then(|v| v.as_i64());
6311 let evaluation_periods = props
6312 .get("EvaluationPeriods")
6313 .and_then(|v| v.as_i64())
6314 .unwrap_or(1);
6315 let datapoints_to_alarm = props.get("DatapointsToAlarm").and_then(|v| v.as_i64());
6316 let threshold = props.get("Threshold").and_then(|v| v.as_f64());
6317 let comparison_operator = props
6318 .get("ComparisonOperator")
6319 .and_then(|v| v.as_str())
6320 .unwrap_or("GreaterThanThreshold")
6321 .to_string();
6322 let treat_missing_data = props
6323 .get("TreatMissingData")
6324 .and_then(|v| v.as_str())
6325 .map(|s| s.to_string());
6326 let evaluate_low_sample_count_percentile = props
6327 .get("EvaluateLowSampleCountPercentile")
6328 .and_then(|v| v.as_str())
6329 .map(|s| s.to_string());
6330
6331 let mut dimensions: BTreeMap<String, String> = BTreeMap::new();
6332 if let Some(arr) = props.get("Dimensions").and_then(|v| v.as_array()) {
6333 for d in arr {
6334 if let (Some(k), Some(v)) = (
6335 d.get("Name").and_then(|x| x.as_str()),
6336 d.get("Value").and_then(|x| x.as_str()),
6337 ) {
6338 dimensions.insert(k.to_string(), v.to_string());
6339 }
6340 }
6341 }
6342
6343 let mut accounts = self.cloudwatch_state.write();
6344 let state = accounts.get_or_create(&self.account_id);
6345 let region_alarms = state.alarms_in_mut(&self.region);
6346 let alarm = region_alarms
6347 .get_mut(&existing.physical_id)
6348 .ok_or_else(|| format!("Alarm {} not found", existing.physical_id))?;
6349 let now = Utc::now();
6350 alarm.alarm_description = alarm_description;
6351 alarm.actions_enabled = actions_enabled;
6352 alarm.ok_actions = ok_actions;
6353 alarm.alarm_actions = alarm_actions;
6354 alarm.insufficient_data_actions = insufficient_data_actions;
6355 alarm.metric_name = metric_name;
6356 alarm.namespace = namespace;
6357 alarm.statistic = statistic;
6358 alarm.extended_statistic = extended_statistic;
6359 alarm.dimensions = dimensions;
6360 alarm.period = period;
6361 alarm.unit = unit;
6362 alarm.evaluation_periods = evaluation_periods;
6363 alarm.datapoints_to_alarm = datapoints_to_alarm;
6364 alarm.threshold = threshold;
6365 alarm.comparison_operator = comparison_operator;
6366 alarm.treat_missing_data = treat_missing_data;
6367 alarm.evaluate_low_sample_count_percentile = evaluate_low_sample_count_percentile;
6368 alarm.configuration_updated_timestamp = now;
6369 alarm.alarm_configuration_updated_timestamp = now;
6370
6371 let alarm_arn = alarm.alarm_arn.clone();
6372 Ok(ProvisionResult::new(existing.physical_id.clone()).with("Arn", alarm_arn))
6373 }
6374
6375 fn update_cloudwatch_dashboard(
6376 &self,
6377 existing: &StackResource,
6378 new_def: &ResourceDefinition,
6379 ) -> Result<ProvisionResult, String> {
6380 let props = &new_def.properties;
6381 if let Some(new_name) = props.get("DashboardName").and_then(|v| v.as_str()) {
6383 if new_name != existing.physical_id {
6384 return Err(
6385 "AWS::CloudWatch::Dashboard updates that change DashboardName require replacement"
6386 .to_string(),
6387 );
6388 }
6389 }
6390 let body = props
6391 .get("DashboardBody")
6392 .ok_or("DashboardBody is required")?;
6393 let body_str = if let Some(s) = body.as_str() {
6394 s.to_string()
6395 } else {
6396 serde_json::to_string(body).map_err(|e| format!("invalid DashboardBody: {e}"))?
6397 };
6398 serde_json::from_str::<serde_json::Value>(&body_str)
6399 .map_err(|e| format!("DashboardBody must be valid JSON: {e}"))?;
6400
6401 let mut accounts = self.cloudwatch_state.write();
6402 let state = accounts.get_or_create(&self.account_id);
6403 let dashboard = state
6404 .dashboards
6405 .get_mut(&existing.physical_id)
6406 .ok_or_else(|| format!("Dashboard {} not found", existing.physical_id))?;
6407 dashboard.size_bytes = body_str.len() as i64;
6408 dashboard.body = body_str;
6409 dashboard.last_modified = Utc::now();
6410 let arn = dashboard.arn.clone();
6411 Ok(ProvisionResult::new(existing.physical_id.clone()).with("Arn", arn))
6412 }
6413
6414 fn create_elbv2_load_balancer(
6417 &self,
6418 resource: &ResourceDefinition,
6419 ) -> Result<ProvisionResult, String> {
6420 let props = &resource.properties;
6421 let name = props
6422 .get("Name")
6423 .and_then(|v| v.as_str())
6424 .unwrap_or(&resource.logical_id)
6425 .to_string();
6426 let scheme = props
6427 .get("Scheme")
6428 .and_then(|v| v.as_str())
6429 .unwrap_or("internet-facing")
6430 .to_string();
6431 let lb_type = props
6432 .get("Type")
6433 .and_then(|v| v.as_str())
6434 .unwrap_or("application")
6435 .to_string();
6436 let ip_address_type = props
6437 .get("IpAddressType")
6438 .and_then(|v| v.as_str())
6439 .unwrap_or("ipv4")
6440 .to_string();
6441 let security_groups: Vec<String> = props
6442 .get("SecurityGroups")
6443 .and_then(|v| v.as_array())
6444 .map(|arr| {
6445 arr.iter()
6446 .filter_map(|s| s.as_str().map(|s| s.to_string()))
6447 .collect()
6448 })
6449 .unwrap_or_default();
6450 let tags = parse_elb_tags(props.get("Tags"));
6451
6452 let mut accounts = self.elbv2_state.write();
6453 let state = accounts.get_or_create(&self.account_id);
6454 let lb_id = Uuid::new_v4().simple().to_string();
6455 let arn = format!(
6456 "arn:aws:elasticloadbalancing:{}:{}:loadbalancer/{}/{}/{}",
6457 self.region,
6458 self.account_id,
6459 if lb_type == "network" { "net" } else { "app" },
6460 name,
6461 &lb_id[..16]
6462 );
6463 let dns_name = format!(
6464 "{}-{}.{}.elb.{}.amazonaws.com",
6465 name,
6466 &lb_id[..16],
6467 self.region,
6468 self.region
6469 );
6470
6471 let mut availability_zones: Vec<fakecloud_elbv2::AvailabilityZone> = Vec::new();
6472 if let Some(arr) = props.get("Subnets").and_then(|v| v.as_array()) {
6473 for s in arr {
6474 if let Some(subnet_id) = s.as_str() {
6475 availability_zones.push(fakecloud_elbv2::AvailabilityZone {
6476 zone_name: format!("{}a", self.region),
6477 subnet_id: subnet_id.to_string(),
6478 outpost_id: None,
6479 load_balancer_addresses: Vec::new(),
6480 source_nat_ipv6_prefixes: Vec::new(),
6481 });
6482 }
6483 }
6484 }
6485
6486 state.load_balancers.insert(
6487 arn.clone(),
6488 LoadBalancer {
6489 arn: arn.clone(),
6490 name: name.clone(),
6491 dns_name: dns_name.clone(),
6492 canonical_hosted_zone_id: "Z2P70J7EXAMPLE".to_string(),
6493 created_time: Utc::now(),
6494 scheme,
6495 vpc_id: String::new(),
6496 state_code: "active".to_string(),
6497 state_reason: None,
6498 lb_type,
6499 availability_zones,
6500 security_groups,
6501 ip_address_type,
6502 customer_owned_ipv4_pool: None,
6503 enforce_security_group_inbound_rules_on_private_link_traffic: None,
6504 enable_prefix_for_ipv6_source_nat: None,
6505 ipv4_ipam_pool_id: None,
6506 tags,
6507 attributes: BTreeMap::new(),
6508 minimum_capacity_units: None,
6509 bound_port: None,
6510 },
6511 );
6512
6513 Ok(ProvisionResult::new(arn.clone())
6514 .with("LoadBalancerArn", arn)
6515 .with(
6516 "LoadBalancerFullName",
6517 format!("app/{name}/{}", &lb_id[..16]),
6518 )
6519 .with("LoadBalancerName", name)
6520 .with("DNSName", dns_name)
6521 .with("CanonicalHostedZoneID", "Z2P70J7EXAMPLE"))
6522 }
6523
6524 fn delete_elbv2_load_balancer(&self, physical_id: &str) -> Result<(), String> {
6525 let mut accounts = self.elbv2_state.write();
6526 let state = accounts.get_or_create(&self.account_id);
6527 state.load_balancers.remove(physical_id);
6528 let listeners: Vec<String> = state
6530 .listeners
6531 .iter()
6532 .filter(|(_, l)| l.load_balancer_arn == physical_id)
6533 .map(|(arn, _)| arn.clone())
6534 .collect();
6535 for arn in &listeners {
6536 state.listeners.remove(arn);
6537 let rules: Vec<String> = state
6538 .rules
6539 .iter()
6540 .filter(|(_, r)| r.listener_arn == *arn)
6541 .map(|(a, _)| a.clone())
6542 .collect();
6543 for r in rules {
6544 state.rules.remove(&r);
6545 }
6546 }
6547 for tg in state.target_groups.values_mut() {
6548 tg.load_balancer_arns.retain(|a| a != physical_id);
6549 }
6550 Ok(())
6551 }
6552
6553 fn create_elbv2_target_group(
6554 &self,
6555 resource: &ResourceDefinition,
6556 ) -> Result<ProvisionResult, String> {
6557 let props = &resource.properties;
6558 let name = props
6559 .get("Name")
6560 .and_then(|v| v.as_str())
6561 .unwrap_or(&resource.logical_id)
6562 .to_string();
6563 let protocol = props
6564 .get("Protocol")
6565 .and_then(|v| v.as_str())
6566 .map(|s| s.to_string());
6567 let port = props.get("Port").and_then(|v| v.as_i64()).map(|n| n as i32);
6568 let vpc_id = props
6569 .get("VpcId")
6570 .and_then(|v| v.as_str())
6571 .map(|s| s.to_string());
6572 let target_type = props
6573 .get("TargetType")
6574 .and_then(|v| v.as_str())
6575 .unwrap_or("instance")
6576 .to_string();
6577 let ip_address_type = props
6578 .get("IpAddressType")
6579 .and_then(|v| v.as_str())
6580 .unwrap_or("ipv4")
6581 .to_string();
6582 let protocol_version = props
6583 .get("ProtocolVersion")
6584 .and_then(|v| v.as_str())
6585 .map(|s| s.to_string());
6586 let tags = parse_elb_tags(props.get("Tags"));
6587
6588 let mut accounts = self.elbv2_state.write();
6589 let state = accounts.get_or_create(&self.account_id);
6590 let id = Uuid::new_v4().simple().to_string();
6591 let arn = format!(
6592 "arn:aws:elasticloadbalancing:{}:{}:targetgroup/{}/{}",
6593 self.region,
6594 self.account_id,
6595 name,
6596 &id[..16]
6597 );
6598
6599 state.target_groups.insert(
6600 arn.clone(),
6601 TargetGroup {
6602 arn: arn.clone(),
6603 name: name.clone(),
6604 protocol,
6605 port,
6606 vpc_id,
6607 target_type,
6608 ip_address_type,
6609 protocol_version,
6610 health_check_protocol: props
6611 .get("HealthCheckProtocol")
6612 .and_then(|v| v.as_str())
6613 .map(|s| s.to_string()),
6614 health_check_port: props
6615 .get("HealthCheckPort")
6616 .and_then(|v| v.as_str())
6617 .map(|s| s.to_string()),
6618 health_check_enabled: props
6619 .get("HealthCheckEnabled")
6620 .and_then(|v| v.as_bool())
6621 .unwrap_or(true),
6622 health_check_path: props
6623 .get("HealthCheckPath")
6624 .and_then(|v| v.as_str())
6625 .map(|s| s.to_string()),
6626 health_check_interval_seconds: props
6627 .get("HealthCheckIntervalSeconds")
6628 .and_then(|v| v.as_i64())
6629 .unwrap_or(30) as i32,
6630 health_check_timeout_seconds: props
6631 .get("HealthCheckTimeoutSeconds")
6632 .and_then(|v| v.as_i64())
6633 .unwrap_or(5) as i32,
6634 healthy_threshold_count: props
6635 .get("HealthyThresholdCount")
6636 .and_then(|v| v.as_i64())
6637 .unwrap_or(5) as i32,
6638 unhealthy_threshold_count: props
6639 .get("UnhealthyThresholdCount")
6640 .and_then(|v| v.as_i64())
6641 .unwrap_or(2) as i32,
6642 matcher_http_code: props
6643 .get("Matcher")
6644 .and_then(|v| v.get("HttpCode"))
6645 .and_then(|v| v.as_str())
6646 .map(|s| s.to_string()),
6647 matcher_grpc_code: props
6648 .get("Matcher")
6649 .and_then(|v| v.get("GrpcCode"))
6650 .and_then(|v| v.as_str())
6651 .map(|s| s.to_string()),
6652 load_balancer_arns: Vec::new(),
6653 targets: Vec::new(),
6654 tags,
6655 attributes: BTreeMap::new(),
6656 created_time: Utc::now(),
6657 },
6658 );
6659
6660 Ok(ProvisionResult::new(arn.clone())
6661 .with("TargetGroupArn", arn)
6662 .with("TargetGroupName", name)
6663 .with("TargetGroupFullName", format!("targetgroup/{}", &id[..16])))
6664 }
6665
6666 fn delete_elbv2_target_group(&self, physical_id: &str) -> Result<(), String> {
6667 let mut accounts = self.elbv2_state.write();
6668 let state = accounts.get_or_create(&self.account_id);
6669 state.target_groups.remove(physical_id);
6670 Ok(())
6671 }
6672
6673 fn create_elbv2_listener(
6674 &self,
6675 resource: &ResourceDefinition,
6676 ) -> Result<ProvisionResult, String> {
6677 let props = &resource.properties;
6678 let load_balancer_arn = props
6679 .get("LoadBalancerArn")
6680 .and_then(|v| v.as_str())
6681 .ok_or_else(|| "LoadBalancerArn is required".to_string())?
6682 .to_string();
6683 let port = props.get("Port").and_then(|v| v.as_i64()).map(|n| n as i32);
6684 let protocol = props
6685 .get("Protocol")
6686 .and_then(|v| v.as_str())
6687 .map(|s| s.to_string());
6688 let default_actions = parse_elb_actions(props.get("DefaultActions"));
6689
6690 let mut accounts = self.elbv2_state.write();
6691 let state = accounts.get_or_create(&self.account_id);
6692 if !state.load_balancers.contains_key(&load_balancer_arn) {
6693 return Err(format!(
6694 "LoadBalancer {load_balancer_arn} not yet provisioned"
6695 ));
6696 }
6697
6698 let lb_full = load_balancer_arn
6699 .rsplit("loadbalancer/")
6700 .next()
6701 .unwrap_or("")
6702 .to_string();
6703 let listener_id = Uuid::new_v4().simple().to_string();
6704 let arn = format!(
6705 "arn:aws:elasticloadbalancing:{}:{}:listener/{}/{}",
6706 self.region,
6707 self.account_id,
6708 lb_full,
6709 &listener_id[..16]
6710 );
6711
6712 for action in &default_actions {
6715 if let Some(tg_arn) = &action.target_group_arn {
6716 if let Some(tg) = state.target_groups.get_mut(tg_arn) {
6717 if !tg.load_balancer_arns.contains(&load_balancer_arn) {
6718 tg.load_balancer_arns.push(load_balancer_arn.clone());
6719 }
6720 }
6721 }
6722 if let Some(forward) = &action.forward {
6723 for tgt in &forward.target_groups {
6724 if let Some(tg) = state.target_groups.get_mut(&tgt.target_group_arn) {
6725 if !tg.load_balancer_arns.contains(&load_balancer_arn) {
6726 tg.load_balancer_arns.push(load_balancer_arn.clone());
6727 }
6728 }
6729 }
6730 }
6731 }
6732
6733 state.listeners.insert(
6734 arn.clone(),
6735 Listener {
6736 arn: arn.clone(),
6737 load_balancer_arn,
6738 port,
6739 protocol,
6740 certificates: Vec::new(),
6741 ssl_policy: props
6742 .get("SslPolicy")
6743 .and_then(|v| v.as_str())
6744 .map(|s| s.to_string()),
6745 default_actions,
6746 alpn_policy: Vec::new(),
6747 mutual_authentication: None,
6748 tags: parse_elb_tags(props.get("Tags")),
6749 attributes: BTreeMap::new(),
6750 },
6751 );
6752
6753 Ok(ProvisionResult::new(arn.clone()).with("ListenerArn", arn))
6754 }
6755
6756 fn delete_elbv2_listener(&self, physical_id: &str) -> Result<(), String> {
6757 let mut accounts = self.elbv2_state.write();
6758 let state = accounts.get_or_create(&self.account_id);
6759 state.listeners.remove(physical_id);
6760 let rules: Vec<String> = state
6761 .rules
6762 .iter()
6763 .filter(|(_, r)| r.listener_arn == physical_id)
6764 .map(|(arn, _)| arn.clone())
6765 .collect();
6766 for r in rules {
6767 state.rules.remove(&r);
6768 }
6769 Ok(())
6770 }
6771
6772 fn create_elbv2_listener_rule(
6773 &self,
6774 resource: &ResourceDefinition,
6775 ) -> Result<ProvisionResult, String> {
6776 let props = &resource.properties;
6777 let listener_arn = props
6778 .get("ListenerArn")
6779 .and_then(|v| v.as_str())
6780 .ok_or_else(|| "ListenerArn is required".to_string())?
6781 .to_string();
6782 let priority = props
6783 .get("Priority")
6784 .map(|v| {
6785 if let Some(s) = v.as_str() {
6786 s.to_string()
6787 } else if let Some(n) = v.as_i64() {
6788 n.to_string()
6789 } else {
6790 "1".to_string()
6791 }
6792 })
6793 .unwrap_or_else(|| "1".to_string());
6794 let actions = parse_elb_actions(props.get("Actions"));
6795 let conditions = parse_elb_rule_conditions(props.get("Conditions"));
6796
6797 let mut accounts = self.elbv2_state.write();
6798 let state = accounts.get_or_create(&self.account_id);
6799 if !state.listeners.contains_key(&listener_arn) {
6800 return Err(format!("Listener {listener_arn} not yet provisioned"));
6801 }
6802 let listener_full = listener_arn
6803 .rsplit("listener/")
6804 .next()
6805 .unwrap_or("")
6806 .to_string();
6807 let rule_id = Uuid::new_v4().simple().to_string();
6808 let arn = format!(
6809 "arn:aws:elasticloadbalancing:{}:{}:listener-rule/{}/{}",
6810 self.region,
6811 self.account_id,
6812 listener_full,
6813 &rule_id[..16]
6814 );
6815
6816 state.rules.insert(
6817 arn.clone(),
6818 ElbRule {
6819 arn: arn.clone(),
6820 listener_arn,
6821 priority,
6822 conditions,
6823 actions,
6824 is_default: false,
6825 tags: parse_elb_tags(props.get("Tags")),
6826 },
6827 );
6828
6829 Ok(ProvisionResult::new(arn.clone()).with("RuleArn", arn))
6830 }
6831
6832 fn delete_elbv2_listener_rule(&self, physical_id: &str) -> Result<(), String> {
6833 let mut accounts = self.elbv2_state.write();
6834 let state = accounts.get_or_create(&self.account_id);
6835 state.rules.remove(physical_id);
6836 Ok(())
6837 }
6838
6839 fn create_elbv2_listener_certificate(
6844 &self,
6845 resource: &ResourceDefinition,
6846 ) -> Result<ProvisionResult, String> {
6847 let props = &resource.properties;
6848 let listener_arn = props
6849 .get("ListenerArn")
6850 .and_then(|v| v.as_str())
6851 .ok_or_else(|| "ListenerArn is required".to_string())?
6852 .to_string();
6853 let certs: Vec<String> = props
6854 .get("Certificates")
6855 .and_then(|v| v.as_array())
6856 .map(|arr| {
6857 arr.iter()
6858 .filter_map(|c| c.get("CertificateArn").and_then(|v| v.as_str()))
6859 .map(|s| s.to_string())
6860 .collect()
6861 })
6862 .unwrap_or_default();
6863 if certs.is_empty() {
6864 return Err("Certificates must contain at least one CertificateArn".to_string());
6865 }
6866 let mut accounts = self.elbv2_state.write();
6867 let state = accounts.get_or_create(&self.account_id);
6868 let listener = state
6869 .listeners
6870 .get_mut(&listener_arn)
6871 .ok_or_else(|| format!("Listener {listener_arn} does not exist"))?;
6872 for arn in &certs {
6873 listener.certificates.retain(|c| &c.certificate_arn != arn);
6874 listener.certificates.push(fakecloud_elbv2::Certificate {
6875 certificate_arn: arn.clone(),
6876 is_default: false,
6877 });
6878 }
6879 Ok(ProvisionResult::new(format!(
6880 "{}#{}",
6881 listener_arn,
6882 certs.join(",")
6883 )))
6884 }
6885
6886 fn delete_elbv2_listener_certificate(&self, physical_id: &str) -> Result<(), String> {
6887 let (listener_arn, cert_list) = match physical_id.split_once('#') {
6888 Some(parts) => parts,
6889 None => return Ok(()),
6890 };
6891 let cert_arns: Vec<&str> = cert_list.split(',').collect();
6892 let mut accounts = self.elbv2_state.write();
6893 let state = accounts.get_or_create(&self.account_id);
6894 if let Some(listener) = state.listeners.get_mut(listener_arn) {
6895 listener
6896 .certificates
6897 .retain(|c| !cert_arns.iter().any(|a| *a == c.certificate_arn));
6898 }
6899 Ok(())
6900 }
6901
6902 fn create_elbv2_trust_store(
6904 &self,
6905 resource: &ResourceDefinition,
6906 ) -> Result<ProvisionResult, String> {
6907 let props = &resource.properties;
6908 let name = props
6909 .get("Name")
6910 .and_then(|v| v.as_str())
6911 .unwrap_or(&resource.logical_id)
6912 .to_string();
6913 let bucket = props
6914 .get("CaCertificatesBundleS3Bucket")
6915 .and_then(|v| v.as_str())
6916 .ok_or_else(|| "CaCertificatesBundleS3Bucket is required".to_string())?;
6917 let key = props
6918 .get("CaCertificatesBundleS3Key")
6919 .and_then(|v| v.as_str())
6920 .ok_or_else(|| "CaCertificatesBundleS3Key is required".to_string())?;
6921 let tags: Vec<fakecloud_elbv2::Tag> = props
6922 .get("Tags")
6923 .and_then(|v| v.as_array())
6924 .map(|arr| {
6925 arr.iter()
6926 .filter_map(|t| {
6927 let k = t.get("Key").and_then(|v| v.as_str())?;
6928 let val = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
6929 Some(fakecloud_elbv2::Tag {
6930 key: k.to_string(),
6931 value: val.to_string(),
6932 })
6933 })
6934 .collect()
6935 })
6936 .unwrap_or_default();
6937
6938 let mut accounts = self.elbv2_state.write();
6939 let state = accounts.get_or_create(&self.account_id);
6940 if state.trust_stores.values().any(|t| t.name == name) {
6941 return Err(format!("Trust store {name} already exists"));
6942 }
6943 let suffix: String = Uuid::new_v4()
6944 .simple()
6945 .to_string()
6946 .chars()
6947 .take(16)
6948 .collect();
6949 let arn = format!(
6950 "arn:aws:elasticloadbalancing:{}:{}:truststore/{}/{}",
6951 self.region, self.account_id, name, suffix
6952 );
6953 let ts = fakecloud_elbv2::TrustStore {
6954 arn: arn.clone(),
6955 name: name.clone(),
6956 status: "ACTIVE".to_string(),
6957 number_of_ca_certificates: 1,
6958 total_revoked_entries: 0,
6959 created_time: Utc::now(),
6960 ca_certificates_bundle: Some(format!("s3://{bucket}/{key}").into_bytes()),
6961 revocations: BTreeMap::new(),
6962 next_revocation_id: 1,
6963 tags,
6964 };
6965 state.trust_stores.insert(arn.clone(), ts);
6966 Ok(ProvisionResult::new(arn.clone())
6967 .with("TrustStoreArn", arn)
6968 .with("Name", name)
6969 .with("Status", "ACTIVE".to_string()))
6970 }
6971
6972 fn delete_elbv2_trust_store(&self, physical_id: &str) -> Result<(), String> {
6973 let mut accounts = self.elbv2_state.write();
6974 let state = accounts.get_or_create(&self.account_id);
6975 state.trust_stores.remove(physical_id);
6976 Ok(())
6977 }
6978
6979 fn update_elbv2_load_balancer(
6984 &self,
6985 existing: &StackResource,
6986 resource: &ResourceDefinition,
6987 ) -> Result<ProvisionResult, String> {
6988 let props = &resource.properties;
6989 let arn = existing.physical_id.clone();
6990 let mut accounts = self.elbv2_state.write();
6991 let state = accounts.get_or_create(&self.account_id);
6992 let lb = state
6993 .load_balancers
6994 .get_mut(&arn)
6995 .ok_or_else(|| format!("LoadBalancer {arn} no longer exists"))?;
6996 if let Some(arr) = props.get("SecurityGroups").and_then(|v| v.as_array()) {
6997 lb.security_groups = arr
6998 .iter()
6999 .filter_map(|s| s.as_str().map(|s| s.to_string()))
7000 .collect();
7001 }
7002 if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
7003 lb.ip_address_type = s.to_string();
7004 }
7005 if let Some(arr) = props.get("Subnets").and_then(|v| v.as_array()) {
7006 let mut zones: Vec<fakecloud_elbv2::AvailabilityZone> = Vec::new();
7007 for s in arr {
7008 if let Some(subnet_id) = s.as_str() {
7009 zones.push(fakecloud_elbv2::AvailabilityZone {
7010 zone_name: format!("{}a", self.region),
7011 subnet_id: subnet_id.to_string(),
7012 outpost_id: None,
7013 load_balancer_addresses: Vec::new(),
7014 source_nat_ipv6_prefixes: Vec::new(),
7015 });
7016 }
7017 }
7018 lb.availability_zones = zones;
7019 }
7020 if props.get("Tags").is_some() {
7021 lb.tags = parse_elb_tags(props.get("Tags"));
7022 }
7023 let name = lb.name.clone();
7024 let dns_name = lb.dns_name.clone();
7025 let canonical = lb.canonical_hosted_zone_id.clone();
7026 let lb_full = arn.rsplit("loadbalancer/").next().unwrap_or("").to_string();
7027 Ok(ProvisionResult::new(arn.clone())
7028 .with("LoadBalancerArn", arn)
7029 .with("LoadBalancerFullName", lb_full)
7030 .with("LoadBalancerName", name)
7031 .with("DNSName", dns_name)
7032 .with("CanonicalHostedZoneID", canonical))
7033 }
7034
7035 fn update_elbv2_target_group(
7038 &self,
7039 existing: &StackResource,
7040 resource: &ResourceDefinition,
7041 ) -> Result<ProvisionResult, String> {
7042 let props = &resource.properties;
7043 let arn = existing.physical_id.clone();
7044 let mut accounts = self.elbv2_state.write();
7045 let state = accounts.get_or_create(&self.account_id);
7046 let tg = state
7047 .target_groups
7048 .get_mut(&arn)
7049 .ok_or_else(|| format!("TargetGroup {arn} no longer exists"))?;
7050 if let Some(s) = props.get("HealthCheckProtocol").and_then(|v| v.as_str()) {
7051 tg.health_check_protocol = Some(s.to_string());
7052 }
7053 if let Some(s) = props.get("HealthCheckPort").and_then(|v| v.as_str()) {
7054 tg.health_check_port = Some(s.to_string());
7055 }
7056 if let Some(b) = props.get("HealthCheckEnabled").and_then(|v| v.as_bool()) {
7057 tg.health_check_enabled = b;
7058 }
7059 if let Some(s) = props.get("HealthCheckPath").and_then(|v| v.as_str()) {
7060 tg.health_check_path = Some(s.to_string());
7061 }
7062 if let Some(n) = props.get("HealthCheckIntervalSeconds").and_then(cfn_as_i64) {
7063 tg.health_check_interval_seconds = n as i32;
7064 }
7065 if let Some(n) = props.get("HealthCheckTimeoutSeconds").and_then(cfn_as_i64) {
7066 tg.health_check_timeout_seconds = n as i32;
7067 }
7068 if let Some(n) = props.get("HealthyThresholdCount").and_then(cfn_as_i64) {
7069 tg.healthy_threshold_count = n as i32;
7070 }
7071 if let Some(n) = props.get("UnhealthyThresholdCount").and_then(cfn_as_i64) {
7072 tg.unhealthy_threshold_count = n as i32;
7073 }
7074 if let Some(matcher) = props.get("Matcher") {
7075 tg.matcher_http_code = matcher
7076 .get("HttpCode")
7077 .and_then(|v| v.as_str())
7078 .map(|s| s.to_string());
7079 tg.matcher_grpc_code = matcher
7080 .get("GrpcCode")
7081 .and_then(|v| v.as_str())
7082 .map(|s| s.to_string());
7083 }
7084 if props.get("Tags").is_some() {
7085 tg.tags = parse_elb_tags(props.get("Tags"));
7086 }
7087 let name = tg.name.clone();
7088 let tg_full = arn
7089 .rsplit("targetgroup/")
7090 .next()
7091 .map(|s| format!("targetgroup/{s}"))
7092 .unwrap_or_default();
7093 Ok(ProvisionResult::new(arn.clone())
7094 .with("TargetGroupArn", arn)
7095 .with("TargetGroupName", name)
7096 .with("TargetGroupFullName", tg_full))
7097 }
7098
7099 fn update_elbv2_listener(
7102 &self,
7103 existing: &StackResource,
7104 resource: &ResourceDefinition,
7105 ) -> Result<ProvisionResult, String> {
7106 let props = &resource.properties;
7107 let arn = existing.physical_id.clone();
7108 let new_default_actions = props
7109 .get("DefaultActions")
7110 .map(|v| parse_elb_actions(Some(v)));
7111 let mut accounts = self.elbv2_state.write();
7112 let state = accounts.get_or_create(&self.account_id);
7113 let listener = state
7114 .listeners
7115 .get_mut(&arn)
7116 .ok_or_else(|| format!("Listener {arn} no longer exists"))?;
7117 if let Some(n) = props.get("Port").and_then(cfn_as_i64) {
7118 listener.port = Some(n as i32);
7119 }
7120 if let Some(s) = props.get("Protocol").and_then(|v| v.as_str()) {
7121 listener.protocol = Some(s.to_string());
7122 }
7123 if let Some(s) = props.get("SslPolicy").and_then(|v| v.as_str()) {
7124 listener.ssl_policy = Some(s.to_string());
7125 }
7126 if let Some(actions) = new_default_actions {
7127 listener.default_actions = actions;
7128 }
7129 if props.get("Tags").is_some() {
7130 listener.tags = parse_elb_tags(props.get("Tags"));
7131 }
7132 Ok(ProvisionResult::new(arn.clone()).with("ListenerArn", arn))
7133 }
7134
7135 fn update_elbv2_listener_rule(
7138 &self,
7139 existing: &StackResource,
7140 resource: &ResourceDefinition,
7141 ) -> Result<ProvisionResult, String> {
7142 let props = &resource.properties;
7143 let arn = existing.physical_id.clone();
7144 let new_actions = props.get("Actions").map(|v| parse_elb_actions(Some(v)));
7145 let new_conditions = props
7146 .get("Conditions")
7147 .map(|v| parse_elb_rule_conditions(Some(v)));
7148 let mut accounts = self.elbv2_state.write();
7149 let state = accounts.get_or_create(&self.account_id);
7150 let rule = state
7151 .rules
7152 .get_mut(&arn)
7153 .ok_or_else(|| format!("ListenerRule {arn} no longer exists"))?;
7154 if let Some(v) = props.get("Priority") {
7155 rule.priority = if let Some(s) = v.as_str() {
7156 s.to_string()
7157 } else if let Some(n) = v.as_i64() {
7158 n.to_string()
7159 } else {
7160 rule.priority.clone()
7161 };
7162 }
7163 if let Some(actions) = new_actions {
7164 rule.actions = actions;
7165 }
7166 if let Some(conditions) = new_conditions {
7167 rule.conditions = conditions;
7168 }
7169 if props.get("Tags").is_some() {
7170 rule.tags = parse_elb_tags(props.get("Tags"));
7171 }
7172 Ok(ProvisionResult::new(arn.clone()).with("RuleArn", arn))
7173 }
7174
7175 fn update_elbv2_listener_certificate(
7180 &self,
7181 existing: &StackResource,
7182 resource: &ResourceDefinition,
7183 ) -> Result<ProvisionResult, String> {
7184 let props = &resource.properties;
7185 let physical_id = existing.physical_id.clone();
7186 let listener_arn = props
7187 .get("ListenerArn")
7188 .and_then(|v| v.as_str())
7189 .map(|s| s.to_string())
7190 .or_else(|| physical_id.split_once('#').map(|(l, _)| l.to_string()))
7191 .ok_or_else(|| "ListenerArn is required".to_string())?;
7192 let new_certs: Vec<String> = props
7193 .get("Certificates")
7194 .and_then(|v| v.as_array())
7195 .map(|arr| {
7196 arr.iter()
7197 .filter_map(|c| c.get("CertificateArn").and_then(|v| v.as_str()))
7198 .map(|s| s.to_string())
7199 .collect()
7200 })
7201 .unwrap_or_default();
7202 if new_certs.is_empty() {
7203 return Err("Certificates must contain at least one CertificateArn".to_string());
7204 }
7205
7206 let prev_certs: Vec<String> = physical_id
7208 .split_once('#')
7209 .map(|(_, list)| list.split(',').map(|s| s.to_string()).collect())
7210 .unwrap_or_default();
7211
7212 let mut accounts = self.elbv2_state.write();
7213 let state = accounts.get_or_create(&self.account_id);
7214 let listener = state
7215 .listeners
7216 .get_mut(&listener_arn)
7217 .ok_or_else(|| format!("Listener {listener_arn} does not exist"))?;
7218 listener
7219 .certificates
7220 .retain(|c| !prev_certs.iter().any(|p| p == &c.certificate_arn));
7221 for arn in &new_certs {
7222 listener.certificates.retain(|c| &c.certificate_arn != arn);
7223 listener.certificates.push(fakecloud_elbv2::Certificate {
7224 certificate_arn: arn.clone(),
7225 is_default: false,
7226 });
7227 }
7228 Ok(ProvisionResult::new(format!(
7229 "{}#{}",
7230 listener_arn,
7231 new_certs.join(",")
7232 )))
7233 }
7234
7235 fn update_elbv2_trust_store(
7238 &self,
7239 existing: &StackResource,
7240 resource: &ResourceDefinition,
7241 ) -> Result<ProvisionResult, String> {
7242 let props = &resource.properties;
7243 let arn = existing.physical_id.clone();
7244 let mut accounts = self.elbv2_state.write();
7245 let state = accounts.get_or_create(&self.account_id);
7246 let ts = state
7247 .trust_stores
7248 .get_mut(&arn)
7249 .ok_or_else(|| format!("TrustStore {arn} no longer exists"))?;
7250 let new_bucket = props
7251 .get("CaCertificatesBundleS3Bucket")
7252 .and_then(|v| v.as_str());
7253 let new_key = props
7254 .get("CaCertificatesBundleS3Key")
7255 .and_then(|v| v.as_str());
7256 if let (Some(b), Some(k)) = (new_bucket, new_key) {
7257 ts.ca_certificates_bundle = Some(format!("s3://{b}/{k}").into_bytes());
7258 }
7259 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
7260 ts.tags = arr
7261 .iter()
7262 .filter_map(|t| {
7263 let k = t.get("Key").and_then(|v| v.as_str())?;
7264 let v = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
7265 Some(fakecloud_elbv2::Tag {
7266 key: k.to_string(),
7267 value: v.to_string(),
7268 })
7269 })
7270 .collect();
7271 }
7272 let name = ts.name.clone();
7273 let status = ts.status.clone();
7274 Ok(ProvisionResult::new(arn.clone())
7275 .with("TrustStoreArn", arn)
7276 .with("Name", name)
7277 .with("Status", status))
7278 }
7279
7280 fn get_att_elbv2_load_balancer(&self, physical_id: &str, attribute: &str) -> Option<String> {
7282 let mut accounts = self.elbv2_state.write();
7283 let state = accounts.get_or_create(&self.account_id);
7284 let lb = state.load_balancers.get(physical_id)?;
7285 let lb_full = lb
7286 .arn
7287 .rsplit("loadbalancer/")
7288 .next()
7289 .unwrap_or("")
7290 .to_string();
7291 match attribute {
7292 "Arn" | "LoadBalancerArn" => Some(lb.arn.clone()),
7293 "DNSName" => Some(lb.dns_name.clone()),
7294 "CanonicalHostedZoneID" => Some(lb.canonical_hosted_zone_id.clone()),
7295 "LoadBalancerFullName" => Some(lb_full),
7296 "LoadBalancerName" => Some(lb.name.clone()),
7297 "SecurityGroups" => Some(lb.security_groups.join(",")),
7298 _ => None,
7299 }
7300 }
7301
7302 fn get_att_elbv2_target_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
7304 let mut accounts = self.elbv2_state.write();
7305 let state = accounts.get_or_create(&self.account_id);
7306 let tg = state.target_groups.get(physical_id)?;
7307 let tg_full = tg
7308 .arn
7309 .rsplit("targetgroup/")
7310 .next()
7311 .map(|s| format!("targetgroup/{s}"))
7312 .unwrap_or_default();
7313 match attribute {
7314 "TargetGroupArn" => Some(tg.arn.clone()),
7315 "TargetGroupName" => Some(tg.name.clone()),
7316 "TargetGroupFullName" => Some(tg_full),
7317 "LoadBalancerArns" => Some(tg.load_balancer_arns.join(",")),
7318 _ => None,
7319 }
7320 }
7321
7322 fn get_att_elbv2_listener(&self, physical_id: &str, attribute: &str) -> Option<String> {
7324 let mut accounts = self.elbv2_state.write();
7325 let state = accounts.get_or_create(&self.account_id);
7326 let listener = state.listeners.get(physical_id)?;
7327 match attribute {
7328 "Arn" | "ListenerArn" => Some(listener.arn.clone()),
7329 _ => None,
7330 }
7331 }
7332
7333 fn get_att_elbv2_listener_rule(&self, physical_id: &str, attribute: &str) -> Option<String> {
7335 let mut accounts = self.elbv2_state.write();
7336 let state = accounts.get_or_create(&self.account_id);
7337 let rule = state.rules.get(physical_id)?;
7338 match attribute {
7339 "RuleArn" => Some(rule.arn.clone()),
7340 "IsDefault" => Some(rule.is_default.to_string()),
7341 _ => None,
7342 }
7343 }
7344
7345 fn get_att_elbv2_trust_store(&self, physical_id: &str, attribute: &str) -> Option<String> {
7347 let mut accounts = self.elbv2_state.write();
7348 let state = accounts.get_or_create(&self.account_id);
7349 let ts = state.trust_stores.get(physical_id)?;
7350 match attribute {
7351 "TrustStoreArn" => Some(ts.arn.clone()),
7352 "Name" => Some(ts.name.clone()),
7353 "Status" => Some(ts.status.clone()),
7354 "NumberOfCaCertificates" => Some(ts.number_of_ca_certificates.to_string()),
7355 "TotalRevokedEntries" => Some(ts.total_revoked_entries.to_string()),
7356 _ => None,
7357 }
7358 }
7359
7360 fn create_organization(
7363 &self,
7364 resource: &ResourceDefinition,
7365 ) -> Result<ProvisionResult, String> {
7366 let props = &resource.properties;
7367 let feature_set = props
7368 .get("FeatureSet")
7369 .and_then(|v| v.as_str())
7370 .unwrap_or("ALL")
7371 .to_string();
7372
7373 let mut org = self.organizations_state.write();
7374 if org.is_some() {
7375 return Err("Organization already exists; only one per fakecloud process".to_string());
7376 }
7377 let mut state = OrganizationState::bootstrap(&self.account_id);
7378 state.feature_set = feature_set;
7379 let org_id = state.org_id.clone();
7380 let org_arn = state.org_arn.clone();
7381 let mgmt_arn = state.management_account_arn.clone();
7382 let root_id = state.root_id.clone();
7383 *org = Some(state);
7384
7385 Ok(ProvisionResult::new(org_id.clone())
7386 .with("Id", org_id)
7387 .with("Arn", org_arn)
7388 .with("ManagementAccountArn", mgmt_arn)
7389 .with("RootId", root_id))
7390 }
7391
7392 fn delete_organization(&self, _physical_id: &str) -> Result<(), String> {
7393 let mut org = self.organizations_state.write();
7394 *org = None;
7395 Ok(())
7396 }
7397
7398 fn create_organization_unit(
7399 &self,
7400 resource: &ResourceDefinition,
7401 ) -> Result<ProvisionResult, String> {
7402 let props = &resource.properties;
7403 let name = props
7404 .get("Name")
7405 .and_then(|v| v.as_str())
7406 .unwrap_or(&resource.logical_id)
7407 .to_string();
7408 let parent_id = props
7409 .get("ParentId")
7410 .and_then(|v| v.as_str())
7411 .ok_or_else(|| "ParentId is required".to_string())?
7412 .to_string();
7413
7414 let mut org_lock = self.organizations_state.write();
7415 let org = org_lock
7416 .as_mut()
7417 .ok_or_else(|| "Organization not yet created".to_string())?;
7418 let resolved_parent_id = if parent_id == org.root_id || org.ous.contains_key(&parent_id) {
7420 parent_id
7421 } else {
7422 return Err(format!("Parent {parent_id} does not exist"));
7423 };
7424 let id_suffix: String = Uuid::new_v4()
7425 .simple()
7426 .to_string()
7427 .chars()
7428 .take(8)
7429 .collect();
7430 let id = format!("ou-{}-{}", &org.root_id[2..], id_suffix);
7431 let arn = format!(
7432 "arn:aws:organizations::{}:ou/{}/{}",
7433 org.management_account_id, org.org_id, id
7434 );
7435 org.ous.insert(
7436 id.clone(),
7437 OrganizationalUnit {
7438 id: id.clone(),
7439 arn: arn.clone(),
7440 name: name.clone(),
7441 parent_id: resolved_parent_id,
7442 },
7443 );
7444 Ok(ProvisionResult::new(id.clone())
7445 .with("Id", id)
7446 .with("Arn", arn)
7447 .with("Name", name))
7448 }
7449
7450 fn delete_organization_unit(&self, physical_id: &str) -> Result<(), String> {
7451 let mut org_lock = self.organizations_state.write();
7452 if let Some(org) = org_lock.as_mut() {
7453 org.ous.remove(physical_id);
7454 org.attachments.remove(physical_id);
7455 }
7456 Ok(())
7457 }
7458
7459 fn create_organization_account(
7463 &self,
7464 resource: &ResourceDefinition,
7465 ) -> Result<ProvisionResult, String> {
7466 let props = &resource.properties;
7467 let email = props
7468 .get("Email")
7469 .and_then(|v| v.as_str())
7470 .ok_or_else(|| "Email is required".to_string())?
7471 .to_string();
7472 let name = props
7473 .get("AccountName")
7474 .and_then(|v| v.as_str())
7475 .ok_or_else(|| "AccountName is required".to_string())?
7476 .to_string();
7477 let parent_ids: Vec<String> = props
7478 .get("ParentIds")
7479 .and_then(|v| v.as_array())
7480 .map(|arr| {
7481 arr.iter()
7482 .filter_map(|v| v.as_str().map(|s| s.to_string()))
7483 .collect()
7484 })
7485 .unwrap_or_default();
7486 let tags: Vec<(String, String)> = props
7487 .get("Tags")
7488 .and_then(|v| v.as_array())
7489 .map(|arr| {
7490 arr.iter()
7491 .filter_map(|t| {
7492 let k = t.get("Key").and_then(|v| v.as_str())?;
7493 let val = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
7494 Some((k.to_string(), val.to_string()))
7495 })
7496 .collect()
7497 })
7498 .unwrap_or_default();
7499
7500 let mut org_lock = self.organizations_state.write();
7501 let org = org_lock
7502 .as_mut()
7503 .ok_or_else(|| "Organization not yet created".to_string())?;
7504 let pending = org.begin_create_account(&email, &name, None);
7509 let status = org.complete_create_account(&pending.id).unwrap_or(pending);
7510 let account_id = status
7511 .account_id
7512 .clone()
7513 .ok_or_else(|| "create_account did not return an account id".to_string())?;
7514 let account_arn = org
7515 .accounts
7516 .get(&account_id)
7517 .map(|a| a.arn.clone())
7518 .unwrap_or_default();
7519 let joined_method = org
7520 .accounts
7521 .get(&account_id)
7522 .map(|a| a.joined_method.clone())
7523 .unwrap_or_else(|| "CREATED".to_string());
7524 let joined_timestamp = org
7525 .accounts
7526 .get(&account_id)
7527 .map(|a| a.joined_timestamp.to_rfc3339())
7528 .unwrap_or_default();
7529 let acct_status = org
7530 .accounts
7531 .get(&account_id)
7532 .map(|a| a.status.clone())
7533 .unwrap_or_else(|| "ACTIVE".to_string());
7534
7535 if let Some(parent) = parent_ids.first() {
7536 let source = org
7537 .accounts
7538 .get(&account_id)
7539 .map(|a| a.parent_id.clone())
7540 .unwrap_or_else(|| org.root_id.clone());
7541 if parent != &source {
7542 org.move_account(&account_id, &source, parent)
7543 .map_err(|e| format!("Failed to move account to parent {parent}: {e:?}"))?;
7544 }
7545 }
7546
7547 if !tags.is_empty() {
7548 org.set_resource_tags(&account_id, &tags);
7549 }
7550
7551 Ok(ProvisionResult::new(account_id.clone())
7552 .with("AccountId", account_id)
7553 .with("AccountName", name)
7554 .with("Email", email)
7555 .with("Arn", account_arn)
7556 .with("JoinedMethod", joined_method)
7557 .with("JoinedTimestamp", joined_timestamp)
7558 .with("Status", acct_status))
7559 }
7560
7561 fn delete_organization_account(&self, physical_id: &str) -> Result<(), String> {
7565 let mut org_lock = self.organizations_state.write();
7566 if let Some(org) = org_lock.as_mut() {
7567 let _ = org.close_account(physical_id);
7568 }
7569 Ok(())
7570 }
7571
7572 fn create_organization_policy(
7573 &self,
7574 resource: &ResourceDefinition,
7575 ) -> Result<ProvisionResult, String> {
7576 let props = &resource.properties;
7577 let name = props
7578 .get("Name")
7579 .and_then(|v| v.as_str())
7580 .unwrap_or(&resource.logical_id)
7581 .to_string();
7582 let description = props
7583 .get("Description")
7584 .and_then(|v| v.as_str())
7585 .unwrap_or("")
7586 .to_string();
7587 let policy_type = props
7588 .get("Type")
7589 .and_then(|v| v.as_str())
7590 .unwrap_or(POLICY_TYPE_SCP)
7591 .to_string();
7592 let content = props
7593 .get("Content")
7594 .map(|v| {
7595 if v.is_string() {
7596 v.as_str().unwrap_or("").to_string()
7597 } else {
7598 serde_json::to_string(v).unwrap_or_default()
7599 }
7600 })
7601 .unwrap_or_default();
7602 let target_ids: Vec<String> = props
7603 .get("TargetIds")
7604 .and_then(|v| v.as_array())
7605 .map(|arr| {
7606 arr.iter()
7607 .filter_map(|t| t.as_str().map(|s| s.to_string()))
7608 .collect()
7609 })
7610 .unwrap_or_default();
7611
7612 let mut org_lock = self.organizations_state.write();
7613 let org = org_lock
7614 .as_mut()
7615 .ok_or_else(|| "Organization not yet created".to_string())?;
7616 let id_suffix: String = Uuid::new_v4()
7617 .simple()
7618 .to_string()
7619 .chars()
7620 .take(8)
7621 .collect();
7622 let id = format!("p-{}", id_suffix);
7623 let arn = format!(
7624 "arn:aws:organizations::{}:policy/{}/{}/{}",
7625 org.management_account_id,
7626 org.org_id,
7627 policy_type.to_lowercase(),
7628 id
7629 );
7630 org.policies.insert(
7631 id.clone(),
7632 OrgPolicy {
7633 id: id.clone(),
7634 arn: arn.clone(),
7635 name: name.clone(),
7636 description,
7637 policy_type,
7638 aws_managed: false,
7639 content,
7640 },
7641 );
7642 for target in target_ids {
7643 org.attachments
7644 .entry(target)
7645 .or_default()
7646 .insert(id.clone());
7647 }
7648 Ok(ProvisionResult::new(id.clone())
7649 .with("Id", id)
7650 .with("Arn", arn)
7651 .with("Name", name))
7652 }
7653
7654 fn delete_organization_policy(&self, physical_id: &str) -> Result<(), String> {
7655 let mut org_lock = self.organizations_state.write();
7656 if let Some(org) = org_lock.as_mut() {
7657 org.policies.remove(physical_id);
7658 for attachments in org.attachments.values_mut() {
7659 attachments.remove(physical_id);
7660 }
7661 }
7662 Ok(())
7663 }
7664
7665 fn create_organization_resource_policy(
7666 &self,
7667 resource: &ResourceDefinition,
7668 ) -> Result<ProvisionResult, String> {
7669 let props = &resource.properties;
7670 let content = props
7671 .get("Content")
7672 .map(|v| {
7673 if v.is_string() {
7674 v.as_str().unwrap_or("").to_string()
7675 } else {
7676 serde_json::to_string(v).unwrap_or_default()
7677 }
7678 })
7679 .ok_or_else(|| "Content is required".to_string())?;
7680
7681 let mut org_lock = self.organizations_state.write();
7682 let org = org_lock
7683 .as_mut()
7684 .ok_or_else(|| "Organization not yet created".to_string())?;
7685 org.resource_policy = Some(content);
7686 let arn = format!(
7687 "arn:aws:organizations::{}:resourcepolicy/{}/rp",
7688 org.management_account_id, org.org_id
7689 );
7690 Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
7691 }
7692
7693 fn delete_organization_resource_policy(&self, _physical_id: &str) -> Result<(), String> {
7694 let mut org_lock = self.organizations_state.write();
7695 if let Some(org) = org_lock.as_mut() {
7696 org.resource_policy = None;
7697 }
7698 Ok(())
7699 }
7700
7701 fn delete_log_group(&self, physical_id: &str) -> Result<(), String> {
7702 let mut logs_accounts = self.logs_state.write();
7703 let state = logs_accounts.default_mut();
7704 let name = state
7706 .log_groups
7707 .iter()
7708 .find(|(_, g)| g.arn == physical_id)
7709 .map(|(name, _)| name.clone());
7710 if let Some(name) = name {
7711 state.log_groups.remove(&name);
7712 }
7713 Ok(())
7714 }
7715
7716 fn create_log_stream(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
7717 let props = &resource.properties;
7718 let log_group_name = props
7719 .get("LogGroupName")
7720 .and_then(|v| v.as_str())
7721 .map(parse_log_group_name)
7722 .ok_or_else(|| "LogGroupName is required".to_string())?;
7723 let log_stream_name = props
7724 .get("LogStreamName")
7725 .and_then(|v| v.as_str())
7726 .unwrap_or(&resource.logical_id)
7727 .to_string();
7728
7729 let mut logs_accounts = self.logs_state.write();
7730 let state = logs_accounts.get_or_create(&self.account_id);
7731 let group = state
7732 .log_groups
7733 .get_mut(&log_group_name)
7734 .ok_or_else(|| format!("Log group {log_group_name} does not exist"))?;
7735 let arn = format!(
7736 "arn:aws:logs:{}:{}:log-group:{}:log-stream:{}",
7737 self.region, self.account_id, log_group_name, log_stream_name
7738 );
7739 if group.log_streams.contains_key(&log_stream_name) {
7740 return Err(format!(
7741 "Log stream {log_stream_name} already exists in {log_group_name}"
7742 ));
7743 }
7744 group.log_streams.insert(
7745 log_stream_name.clone(),
7746 LogStream {
7747 name: log_stream_name.clone(),
7748 arn,
7749 creation_time: Utc::now().timestamp_millis(),
7750 first_event_timestamp: None,
7751 last_event_timestamp: None,
7752 last_ingestion_time: None,
7753 upload_sequence_token: String::new(),
7754 events: Vec::new(),
7755 },
7756 );
7757
7758 let physical_id = format!("{log_group_name}|{log_stream_name}");
7760 Ok(ProvisionResult::new(physical_id))
7761 }
7762
7763 fn delete_log_stream(&self, physical_id: &str) -> Result<(), String> {
7764 let mut logs_accounts = self.logs_state.write();
7765 let state = logs_accounts.get_or_create(&self.account_id);
7766 if let Some((group_name, stream_name)) = physical_id.split_once('|') {
7767 if let Some(group) = state.log_groups.get_mut(group_name) {
7768 group.log_streams.remove(stream_name);
7769 }
7770 }
7771 Ok(())
7772 }
7773
7774 fn create_metric_filter(
7775 &self,
7776 resource: &ResourceDefinition,
7777 ) -> Result<ProvisionResult, String> {
7778 let props = &resource.properties;
7779 let log_group_name = props
7780 .get("LogGroupName")
7781 .and_then(|v| v.as_str())
7782 .map(parse_log_group_name)
7783 .ok_or_else(|| "LogGroupName is required".to_string())?;
7784 let filter_name = props
7785 .get("FilterName")
7786 .and_then(|v| v.as_str())
7787 .unwrap_or(&resource.logical_id)
7788 .to_string();
7789 let filter_pattern = props
7790 .get("FilterPattern")
7791 .and_then(|v| v.as_str())
7792 .unwrap_or("")
7793 .to_string();
7794
7795 let mut transformations: Vec<MetricTransformation> = Vec::new();
7796 if let Some(arr) = props
7797 .get("MetricTransformations")
7798 .and_then(|v| v.as_array())
7799 {
7800 for t in arr {
7801 let metric_name = t
7802 .get("MetricName")
7803 .and_then(|v| v.as_str())
7804 .unwrap_or("")
7805 .to_string();
7806 let metric_namespace = t
7807 .get("MetricNamespace")
7808 .and_then(|v| v.as_str())
7809 .unwrap_or("")
7810 .to_string();
7811 let metric_value = t
7812 .get("MetricValue")
7813 .and_then(|v| v.as_str())
7814 .unwrap_or("1")
7815 .to_string();
7816 let default_value = t.get("DefaultValue").and_then(|v| v.as_f64());
7817 transformations.push(MetricTransformation {
7818 metric_name,
7819 metric_namespace,
7820 metric_value,
7821 default_value,
7822 });
7823 }
7824 }
7825
7826 let mut logs_accounts = self.logs_state.write();
7827 let state = logs_accounts.get_or_create(&self.account_id);
7828 if !state.log_groups.contains_key(&log_group_name) {
7829 return Err(format!("Log group {log_group_name} does not exist"));
7830 }
7831 state
7832 .metric_filters
7833 .retain(|f| !(f.log_group_name == log_group_name && f.filter_name == filter_name));
7834 state.metric_filters.push(MetricFilter {
7835 filter_name: filter_name.clone(),
7836 filter_pattern,
7837 log_group_name: log_group_name.clone(),
7838 metric_transformations: transformations,
7839 creation_time: Utc::now().timestamp_millis(),
7840 });
7841
7842 Ok(ProvisionResult::new(format!(
7843 "{log_group_name}|{filter_name}"
7844 )))
7845 }
7846
7847 fn delete_metric_filter(&self, physical_id: &str) -> Result<(), String> {
7848 let mut logs_accounts = self.logs_state.write();
7849 let state = logs_accounts.get_or_create(&self.account_id);
7850 if let Some((group_name, filter_name)) = physical_id.split_once('|') {
7851 state
7852 .metric_filters
7853 .retain(|f| !(f.log_group_name == group_name && f.filter_name == filter_name));
7854 }
7855 Ok(())
7856 }
7857
7858 fn create_subscription_filter(
7859 &self,
7860 resource: &ResourceDefinition,
7861 ) -> Result<ProvisionResult, String> {
7862 let props = &resource.properties;
7863 let log_group_name = props
7864 .get("LogGroupName")
7865 .and_then(|v| v.as_str())
7866 .map(parse_log_group_name)
7867 .ok_or_else(|| "LogGroupName is required".to_string())?;
7868 let filter_name = props
7869 .get("FilterName")
7870 .and_then(|v| v.as_str())
7871 .unwrap_or(&resource.logical_id)
7872 .to_string();
7873 let filter_pattern = props
7874 .get("FilterPattern")
7875 .and_then(|v| v.as_str())
7876 .unwrap_or("")
7877 .to_string();
7878 let destination_arn = props
7879 .get("DestinationArn")
7880 .and_then(|v| v.as_str())
7881 .ok_or_else(|| "DestinationArn is required".to_string())?
7882 .to_string();
7883 let role_arn = props
7884 .get("RoleArn")
7885 .and_then(|v| v.as_str())
7886 .map(|s| s.to_string());
7887 let distribution = props
7888 .get("Distribution")
7889 .and_then(|v| v.as_str())
7890 .unwrap_or("ByLogStream")
7891 .to_string();
7892
7893 let mut logs_accounts = self.logs_state.write();
7894 let state = logs_accounts.get_or_create(&self.account_id);
7895 let group = state
7896 .log_groups
7897 .get_mut(&log_group_name)
7898 .ok_or_else(|| format!("Log group {log_group_name} does not exist"))?;
7899 group
7900 .subscription_filters
7901 .retain(|f| f.filter_name != filter_name);
7902 group.subscription_filters.push(SubscriptionFilter {
7903 filter_name: filter_name.clone(),
7904 log_group_name: log_group_name.clone(),
7905 filter_pattern,
7906 destination_arn,
7907 role_arn,
7908 distribution,
7909 creation_time: Utc::now().timestamp_millis(),
7910 });
7911
7912 Ok(ProvisionResult::new(format!(
7913 "{log_group_name}|{filter_name}"
7914 )))
7915 }
7916
7917 fn delete_subscription_filter(&self, physical_id: &str) -> Result<(), String> {
7918 let mut logs_accounts = self.logs_state.write();
7919 let state = logs_accounts.get_or_create(&self.account_id);
7920 if let Some((group_name, filter_name)) = physical_id.split_once('|') {
7921 if let Some(group) = state.log_groups.get_mut(group_name) {
7922 group
7923 .subscription_filters
7924 .retain(|f| f.filter_name != filter_name);
7925 }
7926 }
7927 Ok(())
7928 }
7929
7930 fn create_logs_destination(
7933 &self,
7934 resource: &ResourceDefinition,
7935 ) -> Result<ProvisionResult, String> {
7936 let props = &resource.properties;
7937 let destination_name = props
7938 .get("DestinationName")
7939 .and_then(|v| v.as_str())
7940 .ok_or("DestinationName is required")?
7941 .to_string();
7942 let target_arn = props
7943 .get("TargetArn")
7944 .and_then(|v| v.as_str())
7945 .ok_or("TargetArn is required")?
7946 .to_string();
7947 let role_arn = props
7948 .get("RoleArn")
7949 .and_then(|v| v.as_str())
7950 .ok_or("RoleArn is required")?
7951 .to_string();
7952 let access_policy = props
7953 .get("DestinationPolicy")
7954 .and_then(|v| v.as_str())
7955 .map(String::from);
7956
7957 let arn = format!(
7958 "arn:aws:logs:{}:{}:destination:{destination_name}",
7959 self.region, self.account_id
7960 );
7961 let dest = Destination {
7962 destination_name: destination_name.clone(),
7963 target_arn,
7964 role_arn,
7965 arn: arn.clone(),
7966 access_policy,
7967 creation_time: Utc::now().timestamp_millis(),
7968 tags: BTreeMap::new(),
7969 };
7970
7971 let mut logs_accounts = self.logs_state.write();
7972 let state = logs_accounts.get_or_create(&self.account_id);
7973 state.destinations.insert(destination_name.clone(), dest);
7974
7975 Ok(ProvisionResult::new(destination_name).with("Arn", arn))
7976 }
7977
7978 fn delete_logs_destination(&self, physical_id: &str) -> Result<(), String> {
7979 let mut logs_accounts = self.logs_state.write();
7980 let state = logs_accounts.get_or_create(&self.account_id);
7981 state.destinations.remove(physical_id);
7982 Ok(())
7983 }
7984
7985 fn create_logs_resource_policy(
7986 &self,
7987 resource: &ResourceDefinition,
7988 ) -> Result<ProvisionResult, String> {
7989 let props = &resource.properties;
7990 let policy_name = props
7991 .get("PolicyName")
7992 .and_then(|v| v.as_str())
7993 .ok_or("PolicyName is required")?
7994 .to_string();
7995 let policy_document = props
7996 .get("PolicyDocument")
7997 .map(|v| {
7998 if let Some(s) = v.as_str() {
7999 s.to_string()
8000 } else {
8001 serde_json::to_string(v).unwrap_or_default()
8002 }
8003 })
8004 .ok_or("PolicyDocument is required")?;
8005
8006 let policy = ResourcePolicy {
8007 policy_name: policy_name.clone(),
8008 policy_document,
8009 last_updated_time: Utc::now().timestamp_millis(),
8010 };
8011
8012 let mut logs_accounts = self.logs_state.write();
8013 let state = logs_accounts.get_or_create(&self.account_id);
8014 state.resource_policies.insert(policy_name.clone(), policy);
8015
8016 Ok(ProvisionResult::new(policy_name))
8017 }
8018
8019 fn delete_logs_resource_policy(&self, physical_id: &str) -> Result<(), String> {
8020 let mut logs_accounts = self.logs_state.write();
8021 let state = logs_accounts.get_or_create(&self.account_id);
8022 state.resource_policies.remove(physical_id);
8023 Ok(())
8024 }
8025
8026 fn create_logs_query_definition(
8027 &self,
8028 resource: &ResourceDefinition,
8029 ) -> Result<ProvisionResult, String> {
8030 let props = &resource.properties;
8031 let name = props
8032 .get("Name")
8033 .and_then(|v| v.as_str())
8034 .ok_or("Name is required")?
8035 .to_string();
8036 let query_string = props
8037 .get("QueryString")
8038 .and_then(|v| v.as_str())
8039 .ok_or("QueryString is required")?
8040 .to_string();
8041 let log_group_names: Vec<String> = props
8042 .get("LogGroupNames")
8043 .and_then(|v| v.as_array())
8044 .map(|arr| {
8045 arr.iter()
8046 .filter_map(|v| v.as_str().map(String::from))
8047 .collect()
8048 })
8049 .unwrap_or_default();
8050
8051 let id = Uuid::new_v4().to_string();
8052 let qd = QueryDefinition {
8053 query_definition_id: id.clone(),
8054 name,
8055 query_string,
8056 log_group_names,
8057 last_modified: Utc::now().timestamp_millis(),
8058 };
8059
8060 let mut logs_accounts = self.logs_state.write();
8061 let state = logs_accounts.get_or_create(&self.account_id);
8062 state.query_definitions.insert(id.clone(), qd);
8063
8064 Ok(ProvisionResult::new(id.clone()).with("QueryDefinitionId", id))
8065 }
8066
8067 fn delete_logs_query_definition(&self, physical_id: &str) -> Result<(), String> {
8068 let mut logs_accounts = self.logs_state.write();
8069 let state = logs_accounts.get_or_create(&self.account_id);
8070 state.query_definitions.remove(physical_id);
8071 Ok(())
8072 }
8073
8074 fn create_logs_delivery_destination(
8075 &self,
8076 resource: &ResourceDefinition,
8077 ) -> Result<ProvisionResult, String> {
8078 let props = &resource.properties;
8079 let name = props
8080 .get("Name")
8081 .and_then(|v| v.as_str())
8082 .ok_or("Name is required")?
8083 .to_string();
8084 let output_format = props
8085 .get("OutputFormat")
8086 .and_then(|v| v.as_str())
8087 .map(String::from);
8088 let mut configuration: BTreeMap<String, String> = BTreeMap::new();
8089 if let Some(arn) = props.get("DestinationResourceArn").and_then(|v| v.as_str()) {
8090 configuration.insert("destinationResourceArn".to_string(), arn.to_string());
8091 }
8092 if let Some(cfg) = props
8093 .get("DeliveryDestinationConfiguration")
8094 .and_then(|v| v.as_object())
8095 {
8096 for (k, v) in cfg {
8097 if let Some(s) = v.as_str() {
8098 configuration.insert(k.clone(), s.to_string());
8099 }
8100 }
8101 }
8102 let policy = props.get("DeliveryDestinationPolicy").map(|v| {
8103 if let Some(s) = v.as_str() {
8104 s.to_string()
8105 } else {
8106 serde_json::to_string(v).unwrap_or_default()
8107 }
8108 });
8109
8110 let arn = format!(
8111 "arn:aws:logs:{}:{}:delivery-destination:{name}",
8112 self.region, self.account_id
8113 );
8114 let dd = DeliveryDestination {
8115 name: name.clone(),
8116 arn: arn.clone(),
8117 output_format,
8118 delivery_destination_configuration: configuration,
8119 tags: BTreeMap::new(),
8120 delivery_destination_policy: policy,
8121 };
8122
8123 let mut logs_accounts = self.logs_state.write();
8124 let state = logs_accounts.get_or_create(&self.account_id);
8125 state.delivery_destinations.insert(name.clone(), dd);
8126
8127 Ok(ProvisionResult::new(name).with("Arn", arn))
8128 }
8129
8130 fn delete_logs_delivery_destination(&self, physical_id: &str) -> Result<(), String> {
8131 let mut logs_accounts = self.logs_state.write();
8132 let state = logs_accounts.get_or_create(&self.account_id);
8133 state.delivery_destinations.remove(physical_id);
8134 Ok(())
8135 }
8136
8137 fn create_logs_delivery_source(
8138 &self,
8139 resource: &ResourceDefinition,
8140 ) -> Result<ProvisionResult, String> {
8141 let props = &resource.properties;
8142 let name = props
8143 .get("Name")
8144 .and_then(|v| v.as_str())
8145 .ok_or("Name is required")?
8146 .to_string();
8147 let resource_arns: Vec<String> = props
8148 .get("ResourceArn")
8149 .and_then(|v| v.as_str())
8150 .map(|s| vec![s.to_string()])
8151 .or_else(|| {
8152 props
8153 .get("ResourceArns")
8154 .and_then(|v| v.as_array())
8155 .map(|arr| {
8156 arr.iter()
8157 .filter_map(|v| v.as_str().map(String::from))
8158 .collect()
8159 })
8160 })
8161 .unwrap_or_default();
8162 let log_type = props
8163 .get("LogType")
8164 .and_then(|v| v.as_str())
8165 .ok_or("LogType is required")?
8166 .to_string();
8167 let service = props
8168 .get("Service")
8169 .and_then(|v| v.as_str())
8170 .unwrap_or("")
8171 .to_string();
8172
8173 let arn = format!(
8174 "arn:aws:logs:{}:{}:delivery-source:{name}",
8175 self.region, self.account_id
8176 );
8177 let ds = DeliverySource {
8178 name: name.clone(),
8179 arn: arn.clone(),
8180 resource_arns,
8181 service,
8182 log_type,
8183 tags: BTreeMap::new(),
8184 created_at: chrono::Utc::now().timestamp_millis(),
8185 };
8186
8187 let mut logs_accounts = self.logs_state.write();
8188 let state = logs_accounts.get_or_create(&self.account_id);
8189 state.delivery_sources.insert(name.clone(), ds);
8190
8191 Ok(ProvisionResult::new(name).with("Arn", arn))
8192 }
8193
8194 fn delete_logs_delivery_source(&self, physical_id: &str) -> Result<(), String> {
8195 let mut logs_accounts = self.logs_state.write();
8196 let state = logs_accounts.get_or_create(&self.account_id);
8197 state.delivery_sources.remove(physical_id);
8198 Ok(())
8199 }
8200
8201 fn create_logs_delivery(
8202 &self,
8203 resource: &ResourceDefinition,
8204 ) -> Result<ProvisionResult, String> {
8205 let props = &resource.properties;
8206 let delivery_source_name = props
8207 .get("DeliverySourceName")
8208 .and_then(|v| v.as_str())
8209 .ok_or("DeliverySourceName is required")?
8210 .to_string();
8211 let delivery_destination_arn = props
8212 .get("DeliveryDestinationArn")
8213 .and_then(|v| v.as_str())
8214 .ok_or("DeliveryDestinationArn is required")?
8215 .to_string();
8216 let delivery_destination_type = if delivery_destination_arn.contains(":s3:") {
8218 "S3".to_string()
8219 } else if delivery_destination_arn.contains(":firehose:") {
8220 "FH".to_string()
8221 } else {
8222 "CWL".to_string()
8223 };
8224
8225 let id = Uuid::new_v4().simple().to_string();
8226 let arn = format!(
8227 "arn:aws:logs:{}:{}:delivery:{id}",
8228 self.region, self.account_id
8229 );
8230 let delivery = Delivery {
8231 id: id.clone(),
8232 delivery_source_name,
8233 delivery_destination_arn,
8234 delivery_destination_type,
8235 arn: arn.clone(),
8236 tags: BTreeMap::new(),
8237 field_delimiter: None,
8238 record_fields: Vec::new(),
8239 s3_delivery_configuration: None,
8240 created_at: chrono::Utc::now().timestamp_millis(),
8241 };
8242
8243 let mut logs_accounts = self.logs_state.write();
8244 let state = logs_accounts.get_or_create(&self.account_id);
8245 state.deliveries.insert(id.clone(), delivery);
8246
8247 Ok(ProvisionResult::new(id.clone())
8248 .with("DeliveryId", id)
8249 .with("Arn", arn))
8250 }
8251
8252 fn delete_logs_delivery(&self, physical_id: &str) -> Result<(), String> {
8253 let mut logs_accounts = self.logs_state.write();
8254 let state = logs_accounts.get_or_create(&self.account_id);
8255 state.deliveries.remove(physical_id);
8256 Ok(())
8257 }
8258
8259 fn invoke_lambda_sync(&self, function_arn: &str, payload: &str) -> Result<(), String> {
8263 let delivery = self.delivery.clone();
8264 let function_arn = function_arn.to_string();
8265 let payload = payload.to_string();
8266 std::thread::scope(|s| {
8267 s.spawn(|| {
8268 let rt = tokio::runtime::Builder::new_current_thread()
8269 .enable_all()
8270 .build()
8271 .map_err(|e| format!("Failed to create runtime: {e}"))?;
8272 rt.block_on(async {
8273 match delivery.invoke_lambda(&function_arn, &payload).await {
8274 Some(Ok(_)) => {
8275 tracing::info!(
8276 "Custom resource Lambda {} invoked successfully",
8277 function_arn
8278 );
8279 Ok(())
8280 }
8281 Some(Err(e)) => {
8282 tracing::warn!(
8283 "Custom resource Lambda {} invocation failed: {e}",
8284 function_arn
8285 );
8286 Err(format!("Lambda invocation failed: {e}"))
8287 }
8288 None => {
8289 tracing::warn!(
8290 "No Lambda delivery configured; skipping custom resource invocation for {}",
8291 function_arn
8292 );
8293 Ok(())
8294 }
8295 }
8296 })
8297 })
8298 .join()
8299 .map_err(|_| "Lambda invocation thread panicked".to_string())?
8300 })
8301 }
8302
8303 fn create_custom_resource(&self, resource: &ResourceDefinition) -> Result<String, String> {
8304 let props = &resource.properties;
8305 let service_token = props
8306 .get("ServiceToken")
8307 .and_then(|v| v.as_str())
8308 .ok_or("Custom resource requires ServiceToken property")?;
8309
8310 let request_id = Uuid::new_v4().to_string();
8311
8312 let event = serde_json::json!({
8314 "RequestType": "Create",
8315 "ServiceToken": service_token,
8316 "StackId": self.stack_id,
8317 "RequestId": request_id,
8318 "ResourceType": resource.resource_type,
8319 "LogicalResourceId": resource.logical_id,
8320 "ResourceProperties": props,
8321 });
8322
8323 let payload = serde_json::to_string(&event).map_err(|e| e.to_string())?;
8324 self.invoke_lambda_sync(service_token, &payload)?;
8325
8326 let physical_id = format!("{}-{}", resource.logical_id, &request_id[..8]);
8329 Ok(physical_id)
8330 }
8331
8332 fn delete_custom_resource(&self, resource: &StackResource) -> Result<(), String> {
8333 let service_token = match &resource.service_token {
8334 Some(token) => token.clone(),
8335 None => {
8336 return Ok(());
8338 }
8339 };
8340
8341 let request_id = Uuid::new_v4().to_string();
8342
8343 let event = serde_json::json!({
8344 "RequestType": "Delete",
8345 "ServiceToken": service_token,
8346 "StackId": self.stack_id,
8347 "RequestId": request_id,
8348 "ResourceType": resource.resource_type,
8349 "LogicalResourceId": resource.logical_id,
8350 "PhysicalResourceId": resource.physical_id,
8351 });
8352
8353 let payload = serde_json::to_string(&event).map_err(|e| e.to_string())?;
8354
8355 if let Err(e) = self.invoke_lambda_sync(&service_token, &payload) {
8357 tracing::warn!(
8358 "Custom resource delete Lambda invocation failed for {}: {e}",
8359 resource.logical_id
8360 );
8361 }
8362 Ok(())
8363 }
8364
8365 fn create_application_autoscaling_scalable_target(
8368 &self,
8369 resource: &ResourceDefinition,
8370 ) -> Result<ProvisionResult, String> {
8371 let props = &resource.properties;
8372 let service_namespace = props
8373 .get("ServiceNamespace")
8374 .and_then(|v| v.as_str())
8375 .ok_or_else(|| "ServiceNamespace is required".to_string())?
8376 .to_string();
8377 let resource_id = props
8378 .get("ResourceId")
8379 .and_then(|v| v.as_str())
8380 .ok_or_else(|| "ResourceId is required".to_string())?
8381 .to_string();
8382 let scalable_dimension = props
8383 .get("ScalableDimension")
8384 .and_then(|v| v.as_str())
8385 .ok_or_else(|| "ScalableDimension is required".to_string())?
8386 .to_string();
8387 let min_capacity = props
8388 .get("MinCapacity")
8389 .and_then(|v| v.as_i64())
8390 .map(|n| n as i32)
8391 .ok_or_else(|| "MinCapacity is required".to_string())?;
8392 let max_capacity = props
8393 .get("MaxCapacity")
8394 .and_then(|v| v.as_i64())
8395 .map(|n| n as i32)
8396 .ok_or_else(|| "MaxCapacity is required".to_string())?;
8397 if min_capacity > max_capacity {
8398 return Err("MinCapacity must be <= MaxCapacity".to_string());
8399 }
8400 let role_arn = props
8401 .get("RoleARN")
8402 .and_then(|v| v.as_str())
8403 .map(|s| s.to_string());
8404 let suspended_state = props.get("SuspendedState").map(|v| AppasSuspendedState {
8405 dynamic_scaling_in_suspended: v
8406 .get("DynamicScalingInSuspended")
8407 .and_then(|x| x.as_bool()),
8408 dynamic_scaling_out_suspended: v
8409 .get("DynamicScalingOutSuspended")
8410 .and_then(|x| x.as_bool()),
8411 scheduled_scaling_suspended: v
8412 .get("ScheduledScalingSuspended")
8413 .and_then(|x| x.as_bool()),
8414 });
8415
8416 let arn = format!(
8417 "arn:aws:application-autoscaling:{}:{}:scalable-target/{}",
8418 self.region,
8419 self.account_id,
8420 &Uuid::new_v4().simple().to_string()[..10]
8421 );
8422 let role = role_arn.unwrap_or_else(|| {
8423 let suffix = match service_namespace.as_str() {
8424 "ecs" => "ECSService",
8425 "elasticmapreduce" => "EMRContainerService",
8426 "ec2" => "EC2SpotFleetRequest",
8427 "appstream" => "ApplicationAutoScaling_AppStreamFleet",
8428 "dynamodb" => "DynamoDBTable",
8429 "rds" => "RDSCluster",
8430 "sagemaker" => "SageMakerEndpoint",
8431 "lambda" => "LambdaConcurrency",
8432 "elasticache" => "ElastiCacheRG",
8433 "cassandra" => "CassandraTable",
8434 "kafka" => "KafkaCluster",
8435 _ => "ApplicationAutoScaling_Default",
8436 };
8437 format!(
8438 "arn:aws:iam::{}:role/aws-service-role/applicationautoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_{}",
8439 self.account_id, suffix
8440 )
8441 });
8442
8443 let mut state = self.app_autoscaling_state.write();
8444 let account = state.accounts.entry(self.account_id.clone()).or_default();
8445 let key = (
8446 service_namespace.clone(),
8447 resource_id.clone(),
8448 scalable_dimension.clone(),
8449 );
8450 let target = AppasScalableTarget {
8451 arn: arn.clone(),
8452 service_namespace: service_namespace.clone(),
8453 resource_id: resource_id.clone(),
8454 scalable_dimension: scalable_dimension.clone(),
8455 min_capacity,
8456 max_capacity,
8457 role_arn: role,
8458 creation_time: Utc::now(),
8459 suspended_state,
8460 predicted_capacity: None,
8461 };
8462 account.scalable_targets.insert(key, target);
8463
8464 Ok(ProvisionResult::new(resource_id.clone())
8465 .with("ScalableTargetARN", arn)
8466 .with("ServiceNamespace", service_namespace)
8467 .with("ScalableDimension", scalable_dimension))
8468 }
8469
8470 fn create_application_autoscaling_scaling_policy(
8471 &self,
8472 resource: &ResourceDefinition,
8473 ) -> Result<ProvisionResult, String> {
8474 let props = &resource.properties;
8475 let policy_name = props
8476 .get("PolicyName")
8477 .and_then(|v| v.as_str())
8478 .ok_or_else(|| "PolicyName is required".to_string())?
8479 .to_string();
8480 let service_namespace = props
8481 .get("ServiceNamespace")
8482 .and_then(|v| v.as_str())
8483 .ok_or_else(|| "ServiceNamespace is required".to_string())?
8484 .to_string();
8485 let resource_id = props
8486 .get("ResourceId")
8487 .and_then(|v| v.as_str())
8488 .ok_or_else(|| "ResourceId is required".to_string())?
8489 .to_string();
8490 let scalable_dimension = props
8491 .get("ScalableDimension")
8492 .and_then(|v| v.as_str())
8493 .ok_or_else(|| "ScalableDimension is required".to_string())?
8494 .to_string();
8495 let policy_type = props
8496 .get("PolicyType")
8497 .and_then(|v| v.as_str())
8498 .unwrap_or("StepScaling")
8499 .to_string();
8500 let step_cfg = props.get("StepScalingPolicyConfiguration").cloned();
8501 let tt_cfg = props
8502 .get("TargetTrackingScalingPolicyConfiguration")
8503 .cloned();
8504 let pred_cfg = props.get("PredictiveScalingPolicyConfiguration").cloned();
8505
8506 let target_key = (
8507 service_namespace.clone(),
8508 resource_id.clone(),
8509 scalable_dimension.clone(),
8510 );
8511 let policy_key = (
8512 service_namespace.clone(),
8513 resource_id.clone(),
8514 scalable_dimension.clone(),
8515 policy_name.clone(),
8516 );
8517
8518 let mut state = self.app_autoscaling_state.write();
8519 let account = state.accounts.entry(self.account_id.clone()).or_default();
8520 if !account.scalable_targets.contains_key(&target_key) {
8521 return Err(format!(
8522 "No scalable target registered for ServiceNamespace={} ResourceId={} ScalableDimension={}",
8523 service_namespace, resource_id, scalable_dimension
8524 ));
8525 }
8526 let arn = format!(
8527 "arn:aws:autoscaling:{}:{}:scalingPolicy:{}:resource/{}/{}:policyName/{}",
8528 self.region,
8529 self.account_id,
8530 Uuid::new_v4(),
8531 service_namespace,
8532 resource_id,
8533 policy_name
8534 );
8535 let policy = AppasScalingPolicy {
8536 arn: arn.clone(),
8537 policy_name: policy_name.clone(),
8538 service_namespace: service_namespace.clone(),
8539 resource_id: resource_id.clone(),
8540 scalable_dimension: scalable_dimension.clone(),
8541 policy_type: policy_type.clone(),
8542 creation_time: Utc::now(),
8543 step_scaling_policy_configuration: step_cfg,
8544 target_tracking_scaling_policy_configuration: tt_cfg,
8545 predictive_scaling_policy_configuration: pred_cfg,
8546 alarms: Vec::new(),
8547 last_applied_at: None,
8548 };
8549 account.scaling_policies.insert(policy_key, policy);
8550
8551 Ok(ProvisionResult::new(arn.clone())
8552 .with("PolicyName", policy_name)
8553 .with("ServiceNamespace", service_namespace)
8554 .with("ResourceId", resource_id)
8555 .with("ScalableDimension", scalable_dimension))
8556 }
8557
8558 fn delete_application_autoscaling_scalable_target(
8559 &self,
8560 physical_id: &str,
8561 attributes: &BTreeMap<String, String>,
8562 ) -> Result<(), String> {
8563 let namespace = attributes
8564 .get("ServiceNamespace")
8565 .cloned()
8566 .ok_or_else(|| "ServiceNamespace missing in attributes".to_string())?;
8567 let resource_id = physical_id.to_string();
8568 let dimension = attributes
8569 .get("ScalableDimension")
8570 .cloned()
8571 .ok_or_else(|| "ScalableDimension missing in attributes".to_string())?;
8572 let key = (namespace, resource_id.clone(), dimension);
8573
8574 let mut state = self.app_autoscaling_state.write();
8575 let account = state.accounts.entry(self.account_id.clone()).or_default();
8576 account.scalable_targets.remove(&key);
8577 account
8578 .scaling_policies
8579 .retain(|k, _| !(k.0 == key.0 && k.1 == key.1 && k.2 == key.2));
8580 account
8581 .scheduled_actions
8582 .retain(|k, _| !(k.0 == key.0 && k.1 == key.1 && k.2 == key.2));
8583 Ok(())
8584 }
8585
8586 fn delete_application_autoscaling_scaling_policy(
8587 &self,
8588 _physical_id: &str,
8589 attributes: &BTreeMap<String, String>,
8590 ) -> Result<(), String> {
8591 let policy_name = attributes
8592 .get("PolicyName")
8593 .cloned()
8594 .ok_or_else(|| "PolicyName missing in attributes".to_string())?;
8595 let namespace = attributes
8596 .get("ServiceNamespace")
8597 .cloned()
8598 .ok_or_else(|| "ServiceNamespace missing in attributes".to_string())?;
8599 let resource_id = attributes
8600 .get("ResourceId")
8601 .cloned()
8602 .ok_or_else(|| "ResourceId missing in attributes".to_string())?;
8603 let dimension = attributes
8604 .get("ScalableDimension")
8605 .cloned()
8606 .ok_or_else(|| "ScalableDimension missing in attributes".to_string())?;
8607 let key = (namespace, resource_id, dimension, policy_name);
8608
8609 let mut state = self.app_autoscaling_state.write();
8610 let account = state.accounts.entry(self.account_id.clone()).or_default();
8611 account.scaling_policies.remove(&key);
8612 Ok(())
8613 }
8614
8615 fn create_firehose_delivery_stream(
8618 &self,
8619 resource: &ResourceDefinition,
8620 ) -> Result<ProvisionResult, String> {
8621 let props = &resource.properties;
8622 let name = props
8623 .get("DeliveryStreamName")
8624 .and_then(|v| v.as_str())
8625 .unwrap_or(&resource.logical_id)
8626 .to_string();
8627
8628 let arn = format!(
8629 "arn:aws:firehose:{}:{}:deliverystream/{}",
8630 self.region, self.account_id, name
8631 );
8632 let stream_type = props
8633 .get("DeliveryStreamType")
8634 .and_then(|v| v.as_str())
8635 .unwrap_or("DirectPut")
8636 .to_string();
8637
8638 let has_s3 = props.get("S3DestinationConfiguration").is_some();
8639 let has_extended_s3 = props.get("ExtendedS3DestinationConfiguration").is_some();
8640 if has_s3 && has_extended_s3 {
8641 return Err("Only one of S3DestinationConfiguration or ExtendedS3DestinationConfiguration may be set".to_string());
8642 }
8643 let destination = Some(if let Some(s3) = props.get("S3DestinationConfiguration") {
8644 parse_firehose_s3_destination(s3)?
8645 } else if let Some(s3) = props.get("ExtendedS3DestinationConfiguration") {
8646 parse_firehose_s3_destination(s3)?
8647 } else {
8648 return Err("Delivery stream requires a destination configuration".to_string());
8649 });
8650
8651 let mut tags = BTreeMap::new();
8652 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
8653 for tag in arr {
8654 if let (Some(k), Some(v)) = (
8655 tag.get("Key").and_then(|v| v.as_str()),
8656 tag.get("Value").and_then(|v| v.as_str()),
8657 ) {
8658 tags.insert(k.to_string(), v.to_string());
8659 }
8660 }
8661 }
8662
8663 let stream = DeliveryStream {
8664 name: name.clone(),
8665 arn: arn.clone(),
8666 status: "ACTIVE".to_string(),
8667 stream_type: stream_type.clone(),
8668 created_at: Utc::now(),
8669 last_update: Utc::now(),
8670 version_id: "1".to_string(),
8671 destination,
8672 tags,
8673 };
8674
8675 let mut state = self.firehose_state.write();
8676 let account = state.get_or_create(&self.account_id, &self.region);
8677 account
8678 .streams_mut(&self.region)
8679 .insert(name.clone(), stream);
8680
8681 let mut attributes = BTreeMap::new();
8682 attributes.insert("Arn".to_string(), arn.clone());
8683 attributes.insert("DeliveryStreamName".to_string(), name.clone());
8684
8685 Ok(ProvisionResult {
8686 physical_id: name,
8687 attributes,
8688 })
8689 }
8690
8691 fn delete_firehose_delivery_stream(&self, physical_id: &str) -> Result<(), String> {
8692 let mut state = self.firehose_state.write();
8693 let account = state.get_or_create(&self.account_id, &self.region);
8694 account.streams_mut(&self.region).remove(physical_id);
8695 Ok(())
8696 }
8697
8698 fn create_cognito_user_pool(
8701 &self,
8702 resource: &ResourceDefinition,
8703 ) -> Result<ProvisionResult, String> {
8704 let props = &resource.properties;
8705 let pool_name = props
8706 .get("PoolName")
8707 .and_then(|v| v.as_str())
8708 .unwrap_or(&resource.logical_id)
8709 .to_string();
8710
8711 let pool_id = format!(
8712 "{}_{}",
8713 self.region,
8714 Uuid::new_v4()
8715 .simple()
8716 .to_string()
8717 .chars()
8718 .take(9)
8719 .collect::<String>()
8720 );
8721 let arn = format!(
8722 "arn:aws:cognito-idp:{}:{}:userpool/{}",
8723 self.region, self.account_id, pool_id
8724 );
8725 let now = Utc::now();
8726
8727 let password_policy = parse_cognito_password_policy(props.get("Policies"));
8728 let auto_verified = parse_cognito_string_array(props.get("AutoVerifiedAttributes"));
8729 let username_attributes = props
8730 .get("UsernameAttributes")
8731 .and_then(|v| v.as_array())
8732 .map(|_| parse_cognito_string_array(props.get("UsernameAttributes")));
8733 let alias_attributes = props
8734 .get("AliasAttributes")
8735 .and_then(|v| v.as_array())
8736 .map(|_| parse_cognito_string_array(props.get("AliasAttributes")));
8737 let mut schema_attributes = default_schema_attributes();
8738 if let Some(arr) = props.get("Schema").and_then(|v| v.as_array()) {
8739 for attr in arr {
8740 if let Some(parsed) = parse_cognito_schema_attribute(attr) {
8741 if !schema_attributes.iter().any(|a| a.name == parsed.name) {
8742 schema_attributes.push(parsed);
8743 }
8744 }
8745 }
8746 }
8747 let mfa_configuration = props
8748 .get("MfaConfiguration")
8749 .and_then(|v| v.as_str())
8750 .unwrap_or("OFF")
8751 .to_string();
8752 let user_pool_tier = props
8753 .get("UserPoolTier")
8754 .and_then(|v| v.as_str())
8755 .unwrap_or("ESSENTIALS")
8756 .to_string();
8757 let deletion_protection = props
8758 .get("DeletionProtection")
8759 .and_then(|v| v.as_str())
8760 .map(|s| s.to_string());
8761 let user_pool_tags = parse_cognito_tags(props.get("UserPoolTags"));
8762 let email_configuration =
8763 parse_cognito_email_configuration(props.get("EmailConfiguration"));
8764 let sms_configuration = parse_cognito_sms_configuration(props.get("SmsConfiguration"));
8765 let admin_create_user_config =
8766 parse_cognito_admin_create_user_config(props.get("AdminCreateUserConfig"));
8767 let account_recovery_setting =
8768 parse_cognito_account_recovery(props.get("AccountRecoverySetting"));
8769
8770 let signing = fakecloud_cognito::jwt::generate_pool_signing_key();
8774 let signing_key_pem = signing.private_key_pem;
8775 let signing_kid = signing.kid;
8776 let pool = UserPool {
8777 id: pool_id.clone(),
8778 name: pool_name,
8779 arn: arn.clone(),
8780 status: "ACTIVE".to_string(),
8781 creation_date: now,
8782 last_modified_date: now,
8783 policies: PoolPolicies {
8784 password_policy,
8785 sign_in_policy: SignInPolicy {
8786 allowed_first_auth_factors: vec!["PASSWORD".to_string()],
8787 },
8788 },
8789 auto_verified_attributes: auto_verified,
8790 username_attributes,
8791 alias_attributes,
8792 schema_attributes,
8793 lambda_config: None,
8794 mfa_configuration,
8795 email_configuration,
8796 sms_configuration,
8797 admin_create_user_config,
8798 user_pool_tags,
8799 account_recovery_setting,
8800 deletion_protection,
8801 estimated_number_of_users: 0,
8802 software_token_mfa_configuration: None,
8803 sms_mfa_configuration: None,
8804 user_pool_tier,
8805 verification_message_template: None,
8806 signing_key_pem: Some(signing_key_pem),
8807 signing_kid: Some(signing_kid),
8808 };
8809
8810 let mut accounts = self.cognito_state.write();
8811 let state = accounts.get_or_create(&self.account_id);
8812 state.user_pools.insert(pool_id.clone(), pool);
8813
8814 let provider_name = format!("cognito-idp.{}.amazonaws.com/{}", self.region, pool_id);
8815 let provider_url = format!("https://{provider_name}");
8816
8817 Ok(ProvisionResult::new(pool_id.clone())
8818 .with("Arn", arn)
8819 .with("ProviderName", provider_name)
8820 .with("ProviderURL", provider_url)
8821 .with("UserPoolId", pool_id))
8822 }
8823
8824 fn delete_cognito_user_pool(&self, physical_id: &str) -> Result<(), String> {
8825 let mut accounts = self.cognito_state.write();
8826 let state = accounts.get_or_create(&self.account_id);
8827 state.user_pools.remove(physical_id);
8828 state
8830 .user_pool_clients
8831 .retain(|_, c| c.user_pool_id != physical_id);
8832 state.users.remove(physical_id);
8833 state.groups.remove(physical_id);
8834 state.user_groups.remove(physical_id);
8835 state.identity_providers.remove(physical_id);
8836 state.resource_servers.remove(physical_id);
8837 state.import_jobs.remove(physical_id);
8838 state.domains.retain(|_, d| d.user_pool_id != physical_id);
8839 Ok(())
8840 }
8841
8842 fn create_cognito_user_pool_client(
8843 &self,
8844 resource: &ResourceDefinition,
8845 ) -> Result<ProvisionResult, String> {
8846 let props = &resource.properties;
8847 let pool_id = props
8848 .get("UserPoolId")
8849 .and_then(|v| v.as_str())
8850 .ok_or_else(|| "UserPoolId is required".to_string())?
8851 .to_string();
8852 let client_name = props
8853 .get("ClientName")
8854 .and_then(|v| v.as_str())
8855 .unwrap_or(&resource.logical_id)
8856 .to_string();
8857
8858 let mut accounts = self.cognito_state.write();
8859 let state = accounts.get_or_create(&self.account_id);
8860 if !state.user_pools.contains_key(&pool_id) {
8861 return Err(format!(
8863 "User pool {pool_id} does not exist yet — retry once it has been provisioned"
8864 ));
8865 }
8866
8867 let client_id: String = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple())
8868 .chars()
8869 .filter(|c| c.is_ascii_alphanumeric())
8870 .take(26)
8871 .collect::<String>()
8872 .to_lowercase();
8873 let generate_secret = props
8874 .get("GenerateSecret")
8875 .and_then(|v| v.as_bool())
8876 .unwrap_or(false);
8877 let client_secret = if generate_secret {
8878 use base64::Engine;
8879 let mut bytes = Vec::with_capacity(48);
8880 for _ in 0..3 {
8881 bytes.extend_from_slice(Uuid::new_v4().as_bytes());
8882 }
8883 Some(
8884 base64::engine::general_purpose::STANDARD
8885 .encode(&bytes)
8886 .chars()
8887 .take(51)
8888 .collect(),
8889 )
8890 } else {
8891 None
8892 };
8893
8894 let now = Utc::now();
8895 let client = UserPoolClient {
8896 client_id: client_id.clone(),
8897 client_name,
8898 user_pool_id: pool_id.clone(),
8899 client_secret: client_secret.clone(),
8900 explicit_auth_flows: parse_cognito_string_array(props.get("ExplicitAuthFlows")),
8901 token_validity_units: None,
8902 access_token_validity: props.get("AccessTokenValidity").and_then(|v| v.as_i64()),
8903 id_token_validity: props.get("IdTokenValidity").and_then(|v| v.as_i64()),
8904 refresh_token_validity: props.get("RefreshTokenValidity").and_then(|v| v.as_i64()),
8905 callback_urls: parse_cognito_string_array(props.get("CallbackURLs")),
8906 logout_urls: parse_cognito_string_array(props.get("LogoutURLs")),
8907 supported_identity_providers: parse_cognito_string_array(
8908 props.get("SupportedIdentityProviders"),
8909 ),
8910 allowed_o_auth_flows: parse_cognito_string_array(props.get("AllowedOAuthFlows")),
8911 allowed_o_auth_scopes: parse_cognito_string_array(props.get("AllowedOAuthScopes")),
8912 allowed_o_auth_flows_user_pool_client: props
8913 .get("AllowedOAuthFlowsUserPoolClient")
8914 .and_then(|v| v.as_bool())
8915 .unwrap_or(false),
8916 prevent_user_existence_errors: props
8917 .get("PreventUserExistenceErrors")
8918 .and_then(|v| v.as_str())
8919 .map(|s| s.to_string()),
8920 read_attributes: parse_cognito_string_array(props.get("ReadAttributes")),
8921 write_attributes: parse_cognito_string_array(props.get("WriteAttributes")),
8922 creation_date: now,
8923 last_modified_date: now,
8924 enable_token_revocation: props
8925 .get("EnableTokenRevocation")
8926 .and_then(|v| v.as_bool())
8927 .unwrap_or(true),
8928 auth_session_validity: props.get("AuthSessionValidity").and_then(|v| v.as_i64()),
8929 client_secrets: Vec::new(),
8930 refresh_token_rotation: None,
8931 };
8932
8933 state.user_pool_clients.insert(client_id.clone(), client);
8934
8935 let mut result = ProvisionResult::new(client_id.clone())
8936 .with("ClientId", client_id.clone())
8937 .with("Name", client_id);
8938 if let Some(secret) = client_secret {
8939 result = result.with("ClientSecret", secret);
8940 }
8941 Ok(result)
8942 }
8943
8944 fn delete_cognito_user_pool_client(&self, physical_id: &str) -> Result<(), String> {
8945 let mut accounts = self.cognito_state.write();
8946 let state = accounts.get_or_create(&self.account_id);
8947 state.user_pool_clients.remove(physical_id);
8948 Ok(())
8949 }
8950
8951 fn create_cognito_user_pool_domain(
8952 &self,
8953 resource: &ResourceDefinition,
8954 ) -> Result<ProvisionResult, String> {
8955 let props = &resource.properties;
8956 let domain = props
8957 .get("Domain")
8958 .and_then(|v| v.as_str())
8959 .ok_or_else(|| "Domain is required".to_string())?
8960 .to_string();
8961 let pool_id = props
8962 .get("UserPoolId")
8963 .and_then(|v| v.as_str())
8964 .ok_or_else(|| "UserPoolId is required".to_string())?
8965 .to_string();
8966 let custom_domain_config = props
8967 .get("CustomDomainConfig")
8968 .and_then(|v| v.as_object())
8969 .and_then(|m| {
8970 m.get("CertificateArn")
8971 .and_then(|v| v.as_str())
8972 .map(|s| CustomDomainConfig {
8973 certificate_arn: s.to_string(),
8974 })
8975 });
8976
8977 let mut accounts = self.cognito_state.write();
8978 let state = accounts.get_or_create(&self.account_id);
8979 if !state.user_pools.contains_key(&pool_id) {
8980 return Err(format!(
8981 "User pool {pool_id} does not exist yet — retry once it has been provisioned"
8982 ));
8983 }
8984 if state.domains.contains_key(&domain) {
8985 return Err(format!("Domain {domain} already exists"));
8986 }
8987 state.domains.insert(
8988 domain.clone(),
8989 UserPoolDomain {
8990 user_pool_id: pool_id,
8991 domain: domain.clone(),
8992 status: "ACTIVE".to_string(),
8993 custom_domain_config: custom_domain_config.clone(),
8994 creation_date: Utc::now(),
8995 },
8996 );
8997
8998 let cloudfront_distribution = if custom_domain_config.is_some() {
8999 format!("{domain}.cloudfront.net")
9000 } else {
9001 format!("{domain}.auth.{}.amazoncognito.com", self.region)
9002 };
9003
9004 Ok(ProvisionResult::new(domain.clone())
9005 .with("Domain", domain)
9006 .with("CloudFrontDistribution", cloudfront_distribution))
9007 }
9008
9009 fn delete_cognito_user_pool_domain(&self, physical_id: &str) -> Result<(), String> {
9010 let mut accounts = self.cognito_state.write();
9011 let state = accounts.get_or_create(&self.account_id);
9012 state.domains.remove(physical_id);
9013 Ok(())
9014 }
9015
9016 fn create_cognito_identity_pool(
9017 &self,
9018 resource: &ResourceDefinition,
9019 ) -> Result<ProvisionResult, String> {
9020 let props = &resource.properties;
9021 let identity_pool_name = props
9022 .get("IdentityPoolName")
9023 .and_then(|v| v.as_str())
9024 .unwrap_or(&resource.logical_id)
9025 .to_string();
9026 let allow_unauth = props
9027 .get("AllowUnauthenticatedIdentities")
9028 .and_then(|v| v.as_bool())
9029 .unwrap_or(false);
9030 let allow_classic = props
9031 .get("AllowClassicFlow")
9032 .and_then(|v| v.as_bool())
9033 .unwrap_or(false);
9034 let developer_provider_name = props
9035 .get("DeveloperProviderName")
9036 .and_then(|v| v.as_str())
9037 .map(|s| s.to_string());
9038 let cognito_identity_providers = props
9039 .get("CognitoIdentityProviders")
9040 .and_then(|v| v.as_array())
9041 .map(|arr| {
9042 arr.iter()
9043 .filter_map(|p| {
9044 let obj = p.as_object()?;
9045 let provider_name = obj
9046 .get("ProviderName")
9047 .and_then(|v| v.as_str())?
9048 .to_string();
9049 let client_id = obj.get("ClientId").and_then(|v| v.as_str())?.to_string();
9050 let server_side_token_check = obj
9051 .get("ServerSideTokenCheck")
9052 .and_then(|v| v.as_bool())
9053 .unwrap_or(false);
9054 Some(CognitoIdentityProvider {
9055 provider_name,
9056 client_id,
9057 server_side_token_check,
9058 })
9059 })
9060 .collect::<Vec<_>>()
9061 })
9062 .unwrap_or_default();
9063 let open_id_connect_provider_arns =
9064 parse_cognito_string_array(props.get("OpenIdConnectProviderARNs"));
9065 let saml_provider_arns = parse_cognito_string_array(props.get("SamlProviderARNs"));
9066 let supported_login_providers = props
9067 .get("SupportedLoginProviders")
9068 .and_then(|v| v.as_object())
9069 .map(|m| {
9070 m.iter()
9071 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
9072 .collect()
9073 })
9074 .unwrap_or_default();
9075 let identity_pool_tags = parse_cognito_tags(props.get("IdentityPoolTags"));
9076 let cognito_streams = props.get("CognitoStreams").cloned();
9077 let push_sync = props.get("PushSync").cloned();
9078
9079 let identity_pool_id = format!("{}:{}", self.region, Uuid::new_v4());
9082
9083 let pool = IdentityPool {
9084 identity_pool_id: identity_pool_id.clone(),
9085 identity_pool_name,
9086 allow_unauthenticated_identities: allow_unauth,
9087 allow_classic_flow: allow_classic,
9088 developer_provider_name,
9089 cognito_identity_providers,
9090 open_id_connect_provider_arns,
9091 saml_provider_arns,
9092 supported_login_providers,
9093 cognito_streams,
9094 push_sync,
9095 identity_pool_tags,
9096 creation_date: Utc::now(),
9097 };
9098
9099 let mut accounts = self.cognito_state.write();
9100 let state = accounts.get_or_create(&self.account_id);
9101 state.identity_pools.insert(identity_pool_id.clone(), pool);
9102
9103 Ok(ProvisionResult::new(identity_pool_id.clone()).with("Name", identity_pool_id))
9104 }
9105
9106 fn delete_cognito_identity_pool(&self, physical_id: &str) -> Result<(), String> {
9107 let mut accounts = self.cognito_state.write();
9108 let state = accounts.get_or_create(&self.account_id);
9109 state.identity_pools.remove(physical_id);
9110 state
9112 .identity_pool_role_attachments
9113 .retain(|_, a| a.identity_pool_id != physical_id);
9114 Ok(())
9115 }
9116
9117 fn create_cognito_identity_pool_role_attachment(
9118 &self,
9119 resource: &ResourceDefinition,
9120 ) -> Result<ProvisionResult, String> {
9121 let props = &resource.properties;
9122 let identity_pool_id = props
9123 .get("IdentityPoolId")
9124 .and_then(|v| v.as_str())
9125 .ok_or_else(|| "IdentityPoolId is required".to_string())?
9126 .to_string();
9127
9128 let mut accounts = self.cognito_state.write();
9129 let state = accounts.get_or_create(&self.account_id);
9130 if !state.identity_pools.contains_key(&identity_pool_id) {
9131 return Err(format!(
9132 "Identity pool {identity_pool_id} does not exist yet — retry once it has been provisioned"
9133 ));
9134 }
9135
9136 let roles = props
9137 .get("Roles")
9138 .and_then(|v| v.as_object())
9139 .map(|m| {
9140 m.iter()
9141 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
9142 .collect()
9143 })
9144 .unwrap_or_default();
9145 let role_mappings = props
9146 .get("RoleMappings")
9147 .and_then(|v| v.as_object())
9148 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
9149 .unwrap_or_default();
9150
9151 let attachment_id = Uuid::new_v4().simple().to_string();
9152 let physical_id = format!("{identity_pool_id}:{attachment_id}");
9153 let attachment = IdentityPoolRoleAttachment {
9154 identity_pool_id: identity_pool_id.clone(),
9155 attachment_id,
9156 roles,
9157 role_mappings,
9158 };
9159 state
9160 .identity_pool_role_attachments
9161 .insert(physical_id.clone(), attachment);
9162
9163 Ok(ProvisionResult::new(physical_id))
9164 }
9165
9166 fn delete_cognito_identity_pool_role_attachment(
9167 &self,
9168 physical_id: &str,
9169 ) -> Result<(), String> {
9170 let mut accounts = self.cognito_state.write();
9171 let state = accounts.get_or_create(&self.account_id);
9172 state.identity_pool_role_attachments.remove(physical_id);
9173 Ok(())
9174 }
9175
9176 fn create_rds_subnet_group(
9179 &self,
9180 resource: &ResourceDefinition,
9181 ) -> Result<ProvisionResult, String> {
9182 let props = &resource.properties;
9183 let name = props
9184 .get("DBSubnetGroupName")
9185 .and_then(|v| v.as_str())
9186 .unwrap_or(&resource.logical_id)
9187 .to_string();
9188 let description = props
9189 .get("DBSubnetGroupDescription")
9190 .and_then(|v| v.as_str())
9191 .unwrap_or("")
9192 .to_string();
9193 let subnet_ids: Vec<String> = props
9194 .get("SubnetIds")
9195 .and_then(|v| v.as_array())
9196 .map(|arr| {
9197 arr.iter()
9198 .filter_map(|v| v.as_str().map(|s| s.to_string()))
9199 .collect()
9200 })
9201 .unwrap_or_default();
9202 let tags = parse_rds_tags(props.get("Tags"));
9203 let mut accounts = self.rds_state.write();
9204 let state = accounts.get_or_create(&self.account_id);
9205 let arn = state.db_subnet_group_arn(&name);
9206 let group = DbSubnetGroup {
9207 db_subnet_group_name: name.clone(),
9208 db_subnet_group_arn: arn.clone(),
9209 db_subnet_group_description: description,
9210 vpc_id: String::new(),
9211 subnet_ids,
9212 subnet_availability_zones: Vec::new(),
9213 tags,
9214 };
9215 state.subnet_groups.insert(name.clone(), group);
9216 Ok(ProvisionResult::new(name).with("Arn", arn))
9217 }
9218
9219 fn delete_rds_subnet_group(&self, physical_id: &str) -> Result<(), String> {
9220 let mut accounts = self.rds_state.write();
9221 let state = accounts.get_or_create(&self.account_id);
9222 state.subnet_groups.remove(physical_id);
9223 Ok(())
9224 }
9225
9226 fn create_rds_parameter_group(
9227 &self,
9228 resource: &ResourceDefinition,
9229 ) -> Result<ProvisionResult, String> {
9230 let props = &resource.properties;
9231 let name = props
9232 .get("DBParameterGroupName")
9233 .and_then(|v| v.as_str())
9234 .unwrap_or(&resource.logical_id)
9235 .to_string();
9236 let family = props
9237 .get("Family")
9238 .and_then(|v| v.as_str())
9239 .unwrap_or("postgres16")
9240 .to_string();
9241 let description = props
9242 .get("Description")
9243 .and_then(|v| v.as_str())
9244 .unwrap_or("")
9245 .to_string();
9246 let parameters: std::collections::BTreeMap<String, String> = props
9247 .get("Parameters")
9248 .and_then(|v| v.as_object())
9249 .map(|m| {
9250 m.iter()
9251 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
9252 .collect()
9253 })
9254 .unwrap_or_default();
9255 let tags = parse_rds_tags(props.get("Tags"));
9256
9257 let mut accounts = self.rds_state.write();
9258 let state = accounts.get_or_create(&self.account_id);
9259 let arn = state.db_parameter_group_arn(&name);
9260 let group = DbParameterGroup {
9261 db_parameter_group_name: name.clone(),
9262 db_parameter_group_arn: arn.clone(),
9263 db_parameter_group_family: family,
9264 description,
9265 parameters,
9266 tags,
9267 };
9268 state.parameter_groups.insert(name.clone(), group);
9269 Ok(ProvisionResult::new(name).with("Arn", arn))
9270 }
9271
9272 fn delete_rds_parameter_group(&self, physical_id: &str) -> Result<(), String> {
9273 let mut accounts = self.rds_state.write();
9274 let state = accounts.get_or_create(&self.account_id);
9275 state.parameter_groups.remove(physical_id);
9276 Ok(())
9277 }
9278
9279 fn create_rds_cluster_parameter_group(
9280 &self,
9281 resource: &ResourceDefinition,
9282 ) -> Result<ProvisionResult, String> {
9283 let props = &resource.properties;
9284 let name = props
9285 .get("DBClusterParameterGroupName")
9286 .or_else(|| props.get("Name"))
9287 .and_then(|v| v.as_str())
9288 .unwrap_or(&resource.logical_id)
9289 .to_string();
9290 let family = props
9291 .get("Family")
9292 .and_then(|v| v.as_str())
9293 .unwrap_or("aurora-postgresql15")
9294 .to_string();
9295 let description = props
9296 .get("Description")
9297 .and_then(|v| v.as_str())
9298 .unwrap_or("")
9299 .to_string();
9300 let arn = format!(
9301 "arn:aws:rds:{}:{}:cluster-pg:{}",
9302 self.region, self.account_id, name
9303 );
9304 let entry = serde_json::json!({
9305 "DBClusterParameterGroupName": name,
9306 "DBClusterParameterGroupArn": arn,
9307 "DBParameterGroupFamily": family,
9308 "Description": description,
9309 });
9310 let mut accounts = self.rds_state.write();
9311 let state = accounts.get_or_create(&self.account_id);
9312 rds_extras_mut(state, "cluster_param_groups").insert(name.clone(), entry);
9313 Ok(ProvisionResult::new(name).with("Arn", arn))
9314 }
9315
9316 fn delete_rds_cluster_parameter_group(&self, physical_id: &str) -> Result<(), String> {
9317 let mut accounts = self.rds_state.write();
9318 let state = accounts.get_or_create(&self.account_id);
9319 if let Some(m) = state.extras.get_mut("cluster_param_groups") {
9320 m.remove(physical_id);
9321 }
9322 Ok(())
9323 }
9324
9325 fn create_rds_option_group(
9326 &self,
9327 resource: &ResourceDefinition,
9328 ) -> Result<ProvisionResult, String> {
9329 let props = &resource.properties;
9330 let name = props
9331 .get("OptionGroupName")
9332 .and_then(|v| v.as_str())
9333 .unwrap_or(&resource.logical_id)
9334 .to_string();
9335 let engine_name = props
9336 .get("EngineName")
9337 .and_then(|v| v.as_str())
9338 .unwrap_or("mysql")
9339 .to_string();
9340 let major_engine_version = props
9341 .get("MajorEngineVersion")
9342 .and_then(|v| v.as_str())
9343 .unwrap_or("8.0")
9344 .to_string();
9345 let description = props
9346 .get("OptionGroupDescription")
9347 .and_then(|v| v.as_str())
9348 .unwrap_or("")
9349 .to_string();
9350 let arn = format!(
9351 "arn:aws:rds:{}:{}:og:{}",
9352 self.region, self.account_id, name
9353 );
9354 let entry = serde_json::json!({
9355 "OptionGroupName": name,
9356 "OptionGroupArn": arn,
9357 "EngineName": engine_name,
9358 "MajorEngineVersion": major_engine_version,
9359 "OptionGroupDescription": description,
9360 });
9361 let mut accounts = self.rds_state.write();
9362 let state = accounts.get_or_create(&self.account_id);
9363 rds_extras_mut(state, "option_groups").insert(name.clone(), entry);
9364 Ok(ProvisionResult::new(name).with("Arn", arn))
9365 }
9366
9367 fn delete_rds_option_group(&self, physical_id: &str) -> Result<(), String> {
9368 let mut accounts = self.rds_state.write();
9369 let state = accounts.get_or_create(&self.account_id);
9370 if let Some(m) = state.extras.get_mut("option_groups") {
9371 m.remove(physical_id);
9372 }
9373 Ok(())
9374 }
9375
9376 fn create_rds_event_subscription(
9377 &self,
9378 resource: &ResourceDefinition,
9379 ) -> Result<ProvisionResult, String> {
9380 let props = &resource.properties;
9381 let name = props
9382 .get("SubscriptionName")
9383 .and_then(|v| v.as_str())
9384 .unwrap_or(&resource.logical_id)
9385 .to_string();
9386 let sns_topic_arn = props
9387 .get("SnsTopicArn")
9388 .and_then(|v| v.as_str())
9389 .unwrap_or("")
9390 .to_string();
9391 let entry = serde_json::json!({
9392 "CustSubscriptionId": name,
9393 "SnsTopicArn": sns_topic_arn,
9394 "Status": "active",
9395 "Enabled": props.get("Enabled").and_then(|v| v.as_bool()).unwrap_or(true),
9396 });
9397 let mut accounts = self.rds_state.write();
9398 let state = accounts.get_or_create(&self.account_id);
9399 rds_extras_mut(state, "event_subscriptions").insert(name.clone(), entry);
9400 Ok(ProvisionResult::new(name))
9401 }
9402
9403 fn delete_rds_event_subscription(&self, physical_id: &str) -> Result<(), String> {
9404 let mut accounts = self.rds_state.write();
9405 let state = accounts.get_or_create(&self.account_id);
9406 if let Some(m) = state.extras.get_mut("event_subscriptions") {
9407 m.remove(physical_id);
9408 }
9409 Ok(())
9410 }
9411
9412 fn create_rds_security_group(
9413 &self,
9414 resource: &ResourceDefinition,
9415 ) -> Result<ProvisionResult, String> {
9416 let props = &resource.properties;
9417 let name = props
9418 .get("DBSecurityGroupName")
9419 .and_then(|v| v.as_str())
9420 .unwrap_or(&resource.logical_id)
9421 .to_string();
9422 let description = props
9423 .get("GroupDescription")
9424 .and_then(|v| v.as_str())
9425 .unwrap_or("")
9426 .to_string();
9427 let entry = serde_json::json!({
9428 "DBSecurityGroupName": name,
9429 "DBSecurityGroupDescription": description,
9430 "OwnerId": self.account_id,
9431 });
9432 let mut accounts = self.rds_state.write();
9433 let state = accounts.get_or_create(&self.account_id);
9434 rds_extras_mut(state, "security_groups").insert(name.clone(), entry);
9435 Ok(ProvisionResult::new(name))
9436 }
9437
9438 fn delete_rds_security_group(&self, physical_id: &str) -> Result<(), String> {
9439 let mut accounts = self.rds_state.write();
9440 let state = accounts.get_or_create(&self.account_id);
9441 if let Some(m) = state.extras.get_mut("security_groups") {
9442 m.remove(physical_id);
9443 }
9444 Ok(())
9445 }
9446
9447 fn create_rds_db_proxy(
9448 &self,
9449 resource: &ResourceDefinition,
9450 ) -> Result<ProvisionResult, String> {
9451 let props = &resource.properties;
9452 let name = props
9453 .get("DBProxyName")
9454 .and_then(|v| v.as_str())
9455 .unwrap_or(&resource.logical_id)
9456 .to_string();
9457 let engine_family = props
9458 .get("EngineFamily")
9459 .and_then(|v| v.as_str())
9460 .unwrap_or("POSTGRESQL")
9461 .to_string();
9462 let arn = format!(
9463 "arn:aws:rds:{}:{}:db-proxy:{}",
9464 self.region, self.account_id, name
9465 );
9466 let endpoint = format!("{name}.proxy-default.{}.rds.amazonaws.com", self.region);
9467 let entry = serde_json::json!({
9468 "DBProxyName": name,
9469 "DBProxyArn": arn,
9470 "Status": "available",
9471 "EngineFamily": engine_family,
9472 "Endpoint": endpoint,
9473 });
9474 let mut accounts = self.rds_state.write();
9475 let state = accounts.get_or_create(&self.account_id);
9476 rds_extras_mut(state, "proxies").insert(name.clone(), entry);
9477 Ok(ProvisionResult::new(name)
9478 .with("DBProxyArn", arn)
9479 .with("Endpoint", endpoint))
9480 }
9481
9482 fn delete_rds_db_proxy(&self, physical_id: &str) -> Result<(), String> {
9483 let mut accounts = self.rds_state.write();
9484 let state = accounts.get_or_create(&self.account_id);
9485 if let Some(m) = state.extras.get_mut("proxies") {
9486 m.remove(physical_id);
9487 }
9488 Ok(())
9489 }
9490
9491 fn create_rds_db_instance(
9492 &self,
9493 resource: &ResourceDefinition,
9494 ) -> Result<ProvisionResult, String> {
9495 let props = &resource.properties;
9496 let identifier = props
9497 .get("DBInstanceIdentifier")
9498 .and_then(|v| v.as_str())
9499 .map(String::from)
9500 .unwrap_or_else(|| {
9501 format!(
9502 "cfn-{}-{}",
9503 resource.logical_id.to_lowercase(),
9504 Uuid::new_v4().simple().to_string()[..8].to_lowercase()
9505 )
9506 });
9507 let class = props
9508 .get("DBInstanceClass")
9509 .and_then(|v| v.as_str())
9510 .unwrap_or("db.t4g.micro")
9511 .to_string();
9512 let engine = props
9513 .get("Engine")
9514 .and_then(|v| v.as_str())
9515 .unwrap_or("postgres")
9516 .to_string();
9517 let engine_version = props
9518 .get("EngineVersion")
9519 .and_then(|v| v.as_str())
9520 .unwrap_or("16.0")
9521 .to_string();
9522 let master_username = props
9523 .get("MasterUsername")
9524 .and_then(|v| v.as_str())
9525 .unwrap_or("admin")
9526 .to_string();
9527 let master_user_password = props
9528 .get("MasterUserPassword")
9529 .and_then(|v| v.as_str())
9530 .unwrap_or("")
9531 .to_string();
9532 let db_name = props
9533 .get("DBName")
9534 .and_then(|v| v.as_str())
9535 .map(String::from);
9536 let port = props
9537 .get("Port")
9538 .and_then(|v| v.as_i64())
9539 .map(|n| n as i32)
9540 .unwrap_or(5432);
9541 let allocated_storage = props
9542 .get("AllocatedStorage")
9543 .and_then(|v| {
9544 v.as_i64()
9545 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
9546 })
9547 .map(|n| n as i32)
9548 .unwrap_or(20);
9549 let publicly_accessible = props
9550 .get("PubliclyAccessible")
9551 .and_then(|v| v.as_bool())
9552 .unwrap_or(false);
9553 let deletion_protection = props
9554 .get("DeletionProtection")
9555 .and_then(|v| v.as_bool())
9556 .unwrap_or(false);
9557 let backup_retention_period = props
9558 .get("BackupRetentionPeriod")
9559 .and_then(|v| v.as_i64())
9560 .map(|n| n as i32)
9561 .unwrap_or(0);
9562 let multi_az = props
9563 .get("MultiAZ")
9564 .and_then(|v| v.as_bool())
9565 .unwrap_or(false);
9566 let availability_zone = props
9567 .get("AvailabilityZone")
9568 .and_then(|v| v.as_str())
9569 .map(String::from);
9570 let storage_type = props
9571 .get("StorageType")
9572 .and_then(|v| v.as_str())
9573 .map(String::from);
9574 let storage_encrypted = props
9575 .get("StorageEncrypted")
9576 .and_then(|v| v.as_bool())
9577 .unwrap_or(false);
9578 let kms_key_id = props
9579 .get("KmsKeyId")
9580 .and_then(|v| v.as_str())
9581 .map(String::from);
9582 let iam_database_authentication_enabled = props
9583 .get("EnableIAMDatabaseAuthentication")
9584 .and_then(|v| v.as_bool())
9585 .unwrap_or(false);
9586 let db_parameter_group_name = props
9587 .get("DBParameterGroupName")
9588 .and_then(|v| v.as_str())
9589 .map(String::from);
9590 let option_group_name = props
9591 .get("OptionGroupName")
9592 .and_then(|v| v.as_str())
9593 .map(String::from);
9594 let vpc_security_group_ids: Vec<String> = props
9595 .get("VPCSecurityGroups")
9596 .and_then(|v| v.as_array())
9597 .map(|arr| {
9598 arr.iter()
9599 .filter_map(|v| v.as_str().map(String::from))
9600 .collect()
9601 })
9602 .unwrap_or_default();
9603 let enabled_cloudwatch_logs_exports: Vec<String> = props
9604 .get("EnableCloudwatchLogsExports")
9605 .and_then(|v| v.as_array())
9606 .map(|arr| {
9607 arr.iter()
9608 .filter_map(|v| v.as_str().map(String::from))
9609 .collect()
9610 })
9611 .unwrap_or_default();
9612 let tags = parse_rds_tags(props.get("Tags"));
9613
9614 let mut accounts = self.rds_state.write();
9615 let state = accounts.get_or_create(&self.account_id);
9616 let arn = state.db_instance_arn(&identifier);
9617 let endpoint_address = format!(
9618 "{identifier}.cluster-fakecloud.{}.rds.amazonaws.com",
9619 state.region
9620 );
9621 let dbi_resource_id = format!("db-{}", Uuid::new_v4().simple());
9622 let inst = DbInstance {
9623 db_instance_identifier: identifier.clone(),
9624 db_instance_arn: arn.clone(),
9625 db_instance_class: class,
9626 engine,
9627 engine_version,
9628 db_instance_status: "available".to_string(),
9629 master_username,
9630 db_name,
9631 endpoint_address,
9632 port,
9633 allocated_storage,
9634 publicly_accessible,
9635 deletion_protection,
9636 created_at: Utc::now(),
9637 dbi_resource_id,
9638 master_user_password,
9639 container_id: String::new(),
9640 host_port: 0,
9641 tags,
9642 read_replica_source_db_instance_identifier: None,
9643 read_replica_db_instance_identifiers: Vec::new(),
9644 vpc_security_group_ids,
9645 db_parameter_group_name,
9646 backup_retention_period,
9647 preferred_backup_window: "03:00-04:00".to_string(),
9648 preferred_maintenance_window: None,
9649 latest_restorable_time: None,
9650 option_group_name,
9651 multi_az,
9652 pending_modified_values: None,
9653 availability_zone,
9654 storage_type,
9655 storage_encrypted,
9656 kms_key_id,
9657 iam_database_authentication_enabled,
9658 iops: props.get("Iops").and_then(|v| v.as_i64()).map(|n| n as i32),
9659 monitoring_interval: props
9660 .get("MonitoringInterval")
9661 .and_then(|v| v.as_i64())
9662 .map(|n| n as i32),
9663 monitoring_role_arn: props
9664 .get("MonitoringRoleArn")
9665 .and_then(|v| v.as_str())
9666 .map(String::from),
9667 performance_insights_enabled: props
9668 .get("EnablePerformanceInsights")
9669 .and_then(|v| v.as_bool())
9670 .unwrap_or(false),
9671 performance_insights_kms_key_id: props
9672 .get("PerformanceInsightsKMSKeyId")
9673 .and_then(|v| v.as_str())
9674 .map(String::from),
9675 performance_insights_retention_period: props
9676 .get("PerformanceInsightsRetentionPeriod")
9677 .and_then(|v| v.as_i64())
9678 .map(|n| n as i32),
9679 enabled_cloudwatch_logs_exports,
9680 ca_certificate_identifier: props
9681 .get("CACertificateIdentifier")
9682 .and_then(|v| v.as_str())
9683 .map(String::from),
9684 network_type: props
9685 .get("NetworkType")
9686 .and_then(|v| v.as_str())
9687 .map(String::from),
9688 character_set_name: props
9689 .get("CharacterSetName")
9690 .and_then(|v| v.as_str())
9691 .map(String::from),
9692 auto_minor_version_upgrade: props
9693 .get("AutoMinorVersionUpgrade")
9694 .and_then(|v| v.as_bool()),
9695 copy_tags_to_snapshot: props.get("CopyTagsToSnapshot").and_then(|v| v.as_bool()),
9696 master_user_secret_arn: None,
9697 master_user_secret_kms_key_id: props
9698 .get("MasterUserSecret")
9699 .and_then(|v| v.get("KmsKeyId"))
9700 .and_then(|v| v.as_str())
9701 .map(String::from),
9702 license_model: props
9703 .get("LicenseModel")
9704 .and_then(|v| v.as_str())
9705 .map(String::from),
9706 max_allocated_storage: props
9707 .get("MaxAllocatedStorage")
9708 .and_then(|v| v.as_i64())
9709 .map(|n| n as i32),
9710 multi_tenant: props.get("MultiTenant").and_then(|v| v.as_bool()),
9711 storage_throughput: props
9712 .get("StorageThroughput")
9713 .and_then(|v| v.as_i64())
9714 .map(|n| n as i32),
9715 tde_credential_arn: props
9716 .get("TdeCredentialArn")
9717 .and_then(|v| v.as_str())
9718 .map(String::from),
9719 delete_automated_backups: props
9720 .get("DeleteAutomatedBackups")
9721 .and_then(|v| v.as_bool()),
9722 db_security_groups: props
9723 .get("DBSecurityGroups")
9724 .and_then(|v| v.as_array())
9725 .map(|arr| {
9726 arr.iter()
9727 .filter_map(|v| v.as_str().map(String::from))
9728 .collect()
9729 })
9730 .unwrap_or_default(),
9731 domain: props
9732 .get("Domain")
9733 .and_then(|v| v.as_str())
9734 .map(String::from),
9735 domain_fqdn: props
9736 .get("DomainFqdn")
9737 .and_then(|v| v.as_str())
9738 .map(String::from),
9739 domain_ou: props
9740 .get("DomainOu")
9741 .and_then(|v| v.as_str())
9742 .map(String::from),
9743 domain_iam_role_name: props
9744 .get("DomainIAMRoleName")
9745 .and_then(|v| v.as_str())
9746 .map(String::from),
9747 domain_auth_secret_arn: props
9748 .get("DomainAuthSecretArn")
9749 .and_then(|v| v.as_str())
9750 .map(String::from),
9751 domain_dns_ips: props
9752 .get("DomainDnsIps")
9753 .and_then(|v| v.as_array())
9754 .map(|arr| {
9755 arr.iter()
9756 .filter_map(|v| v.as_str().map(String::from))
9757 .collect()
9758 })
9759 .unwrap_or_default(),
9760 db_cluster_identifier: props
9761 .get("DBClusterIdentifier")
9762 .and_then(|v| v.as_str())
9763 .map(String::from),
9764 };
9765 let endpoint = inst.endpoint_address.clone();
9766 let endpoint_port = inst.port;
9767 state.instances.insert(identifier.clone(), inst);
9768
9769 Ok(ProvisionResult::new(identifier.clone())
9770 .with("DBInstanceArn", arn)
9771 .with("Endpoint.Address", endpoint)
9772 .with("Endpoint.Port", endpoint_port.to_string())
9773 .with("DbiResourceId", format!("db-{identifier}")))
9774 }
9775
9776 fn delete_rds_db_instance(&self, physical_id: &str) -> Result<(), String> {
9777 let mut accounts = self.rds_state.write();
9778 let state = accounts.get_or_create(&self.account_id);
9779 state.instances.remove(physical_id);
9780 Ok(())
9781 }
9782
9783 fn create_rds_db_cluster(
9784 &self,
9785 resource: &ResourceDefinition,
9786 ) -> Result<ProvisionResult, String> {
9787 let props = &resource.properties;
9788 let identifier = props
9789 .get("DBClusterIdentifier")
9790 .and_then(|v| v.as_str())
9791 .map(String::from)
9792 .unwrap_or_else(|| {
9793 format!(
9794 "cfn-cluster-{}-{}",
9795 resource.logical_id.to_lowercase(),
9796 Uuid::new_v4().simple().to_string()[..8].to_lowercase()
9797 )
9798 });
9799 let engine = props
9800 .get("Engine")
9801 .and_then(|v| v.as_str())
9802 .unwrap_or("aurora-postgresql")
9803 .to_string();
9804 let engine_version = props
9805 .get("EngineVersion")
9806 .and_then(|v| v.as_str())
9807 .map(String::from);
9808 let master_username = props
9809 .get("MasterUsername")
9810 .and_then(|v| v.as_str())
9811 .map(String::from);
9812 let port = props.get("Port").and_then(|v| v.as_i64()).unwrap_or(5432);
9813 let mut accounts = self.rds_state.write();
9814 let state = accounts.get_or_create(&self.account_id);
9815 let arn = format!(
9816 "arn:aws:rds:{}:{}:cluster:{}",
9817 state.region, state.account_id, identifier
9818 );
9819 let cluster_resource_id = format!("cluster-{}", Uuid::new_v4().simple());
9820 let endpoint = format!(
9821 "{identifier}.cluster-fakecloud.{}.rds.amazonaws.com",
9822 state.region
9823 );
9824 let reader_endpoint = format!(
9825 "{identifier}.cluster-ro-fakecloud.{}.rds.amazonaws.com",
9826 state.region
9827 );
9828 let body = serde_json::json!({
9829 "DBClusterIdentifier": identifier,
9830 "DBClusterArn": arn,
9831 "Engine": engine,
9832 "EngineVersion": engine_version,
9833 "MasterUsername": master_username,
9834 "Status": "available",
9835 "DbClusterResourceId": cluster_resource_id,
9836 "Endpoint": endpoint,
9837 "ReaderEndpoint": reader_endpoint,
9838 "Port": port,
9839 "AllocatedStorage": props.get("AllocatedStorage").and_then(|v| v.as_i64()).unwrap_or(1),
9840 "BackupRetentionPeriod": props.get("BackupRetentionPeriod").and_then(|v| v.as_i64()).unwrap_or(1),
9841 "DatabaseName": props.get("DatabaseName").and_then(|v| v.as_str()),
9842 "DBSubnetGroup": props.get("DBSubnetGroupName").and_then(|v| v.as_str()),
9843 "VpcSecurityGroupIds": props.get("VpcSecurityGroupIds").cloned().unwrap_or(serde_json::json!([])),
9844 "StorageEncrypted": props.get("StorageEncrypted").and_then(|v| v.as_bool()).unwrap_or(false),
9845 "KmsKeyId": props.get("KmsKeyId").and_then(|v| v.as_str()),
9846 "DeletionProtection": props.get("DeletionProtection").and_then(|v| v.as_bool()).unwrap_or(false),
9847 "ClusterCreateTime": Utc::now().to_rfc3339(),
9848 "EnabledCloudwatchLogsExports": props.get("EnableCloudwatchLogsExports").cloned().unwrap_or(serde_json::json!([])),
9849 "MultiAZ": false,
9850 "DBClusterMembers": [],
9851 });
9852 state
9853 .extras
9854 .entry("clusters".to_string())
9855 .or_default()
9856 .insert(identifier.clone(), body);
9857 Ok(ProvisionResult::new(identifier.clone())
9858 .with("DBClusterArn", arn)
9859 .with("Endpoint.Address", endpoint)
9860 .with("ReadEndpoint.Address", reader_endpoint)
9861 .with("Endpoint.Port", port.to_string())
9862 .with("DBClusterResourceId", cluster_resource_id))
9863 }
9864
9865 fn delete_rds_db_cluster(&self, physical_id: &str) -> Result<(), String> {
9866 let mut accounts = self.rds_state.write();
9867 let state = accounts.get_or_create(&self.account_id);
9868 if let Some(m) = state.extras.get_mut("clusters") {
9869 m.remove(physical_id);
9870 }
9871 Ok(())
9872 }
9873
9874 fn create_ecs_cluster(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
9877 let props = &resource.properties;
9878 let cluster_name = props
9879 .get("ClusterName")
9880 .and_then(|v| v.as_str())
9881 .unwrap_or(&resource.logical_id)
9882 .to_string();
9883 let cluster_arn = format!(
9884 "arn:aws:ecs:{}:{}:cluster/{}",
9885 self.region, self.account_id, cluster_name
9886 );
9887 let mut cluster = EcsCluster::new(&cluster_name, cluster_arn.clone());
9888 cluster.tags = parse_ecs_tags(props.get("Tags"));
9889 cluster.capacity_providers = props
9890 .get("CapacityProviders")
9891 .and_then(|v| v.as_array())
9892 .map(|arr| {
9893 arr.iter()
9894 .filter_map(|v| v.as_str().map(|s| s.to_string()))
9895 .collect()
9896 })
9897 .unwrap_or_default();
9898 if let Some(strategy) = props
9899 .get("DefaultCapacityProviderStrategy")
9900 .and_then(|v| v.as_array())
9901 {
9902 cluster.default_capacity_provider_strategy =
9903 strategy.iter().cloned().map(lowercase_first_keys).collect();
9904 }
9905 if let Some(cfg) = props.get("Configuration") {
9906 cluster.configuration = Some(lowercase_first_keys(cfg.clone()));
9907 }
9908 if let Some(settings) = props.get("ClusterSettings").and_then(|v| v.as_array()) {
9909 cluster.settings = settings.iter().cloned().map(lowercase_first_keys).collect();
9910 }
9911 if let Some(scd) = props.get("ServiceConnectDefaults") {
9912 cluster.service_connect_defaults = Some(lowercase_first_keys(scd.clone()));
9913 }
9914 let mut accounts = self.ecs_state.write();
9915 let state = accounts.get_or_create(&self.account_id);
9916 state.clusters.insert(cluster_name.clone(), cluster);
9917 Ok(ProvisionResult::new(cluster_name).with("Arn", cluster_arn))
9918 }
9919
9920 fn delete_ecs_cluster(&self, physical_id: &str) -> Result<(), String> {
9921 let mut accounts = self.ecs_state.write();
9922 let state = accounts.get_or_create(&self.account_id);
9923 state.clusters.remove(physical_id);
9924 state.services.retain(|_, s| s.cluster_name != physical_id);
9926 state
9927 .tasks
9928 .retain(|_, t| t.cluster_arn.split('/').next_back() != Some(physical_id));
9929 Ok(())
9930 }
9931
9932 fn create_ecs_task_definition(
9933 &self,
9934 resource: &ResourceDefinition,
9935 ) -> Result<ProvisionResult, String> {
9936 let props = &resource.properties;
9937 let family = props
9938 .get("Family")
9939 .and_then(|v| v.as_str())
9940 .unwrap_or(&resource.logical_id)
9941 .to_string();
9942 let container_definitions: Vec<serde_json::Value> = props
9945 .get("ContainerDefinitions")
9946 .and_then(|v| v.as_array())
9947 .cloned()
9948 .unwrap_or_default()
9949 .into_iter()
9950 .map(lowercase_first_keys)
9951 .collect();
9952 let task_role_arn = props
9953 .get("TaskRoleArn")
9954 .and_then(|v| v.as_str())
9955 .map(|s| s.to_string());
9956 let execution_role_arn = props
9957 .get("ExecutionRoleArn")
9958 .and_then(|v| v.as_str())
9959 .map(|s| s.to_string());
9960 let network_mode = props
9961 .get("NetworkMode")
9962 .and_then(|v| v.as_str())
9963 .map(|s| s.to_string());
9964 let requires_compatibilities: Vec<String> = props
9965 .get("RequiresCompatibilities")
9966 .and_then(|v| v.as_array())
9967 .map(|arr| {
9968 arr.iter()
9969 .filter_map(|v| v.as_str().map(|s| s.to_string()))
9970 .collect()
9971 })
9972 .unwrap_or_default();
9973 let cpu = props
9974 .get("Cpu")
9975 .and_then(|v| v.as_str())
9976 .map(|s| s.to_string());
9977 let memory = props
9978 .get("Memory")
9979 .and_then(|v| v.as_str())
9980 .map(|s| s.to_string());
9981 let volumes: Vec<serde_json::Value> = props
9982 .get("Volumes")
9983 .and_then(|v| v.as_array())
9984 .cloned()
9985 .unwrap_or_default()
9986 .into_iter()
9987 .map(lowercase_first_keys)
9988 .collect();
9989 let placement_constraints: Vec<serde_json::Value> = props
9990 .get("PlacementConstraints")
9991 .and_then(|v| v.as_array())
9992 .cloned()
9993 .unwrap_or_default()
9994 .into_iter()
9995 .map(lowercase_first_keys)
9996 .collect();
9997 let proxy_configuration = props
9998 .get("ProxyConfiguration")
9999 .cloned()
10000 .map(lowercase_first_keys);
10001 let ephemeral_storage = props
10002 .get("EphemeralStorage")
10003 .cloned()
10004 .map(lowercase_first_keys);
10005 let runtime_platform = props
10006 .get("RuntimePlatform")
10007 .cloned()
10008 .map(lowercase_first_keys);
10009 let tags = parse_ecs_tags(props.get("Tags"));
10010
10011 let mut accounts = self.ecs_state.write();
10012 let state = accounts.get_or_create(&self.account_id);
10013 let revision = state
10014 .next_revision
10015 .entry(family.clone())
10016 .and_modify(|n| *n += 1)
10017 .or_insert(1);
10018 let revision = *revision;
10019 let arn = format!(
10020 "arn:aws:ecs:{}:{}:task-definition/{}:{}",
10021 self.region, self.account_id, family, revision
10022 );
10023 let td = EcsTaskDefinition {
10024 family: family.clone(),
10025 revision,
10026 task_definition_arn: arn.clone(),
10027 container_definitions,
10028 status: "ACTIVE".to_string(),
10029 task_role_arn,
10030 execution_role_arn,
10031 network_mode,
10032 requires_compatibilities: requires_compatibilities.clone(),
10033 compatibilities: requires_compatibilities,
10034 cpu,
10035 memory,
10036 pid_mode: None,
10037 ipc_mode: None,
10038 volumes,
10039 placement_constraints,
10040 proxy_configuration,
10041 inference_accelerators: Vec::new(),
10042 ephemeral_storage,
10043 runtime_platform,
10044 requires_attributes: Vec::new(),
10045 registered_at: Utc::now(),
10046 registered_by: None,
10047 deregistered_at: None,
10048 tags,
10049 enable_fault_injection: props.get("EnableFaultInjection").and_then(|v| v.as_bool()),
10050 };
10051 state
10052 .task_definitions
10053 .entry(family.clone())
10054 .or_default()
10055 .insert(revision, td);
10056 Ok(ProvisionResult::new(arn.clone()).with("TaskDefinitionArn", arn))
10057 }
10058
10059 fn delete_ecs_task_definition(&self, physical_id: &str) -> Result<(), String> {
10060 let Some(suffix) = physical_id.rsplit('/').next() else {
10063 return Ok(());
10064 };
10065 let Some((family, rev)) = suffix.split_once(':') else {
10066 return Ok(());
10067 };
10068 let Ok(revision) = rev.parse::<i32>() else {
10069 return Ok(());
10070 };
10071 let mut accounts = self.ecs_state.write();
10072 let state = accounts.get_or_create(&self.account_id);
10073 if let Some(revs) = state.task_definitions.get_mut(family) {
10074 if let Some(td) = revs.get_mut(&revision) {
10075 td.status = "INACTIVE".to_string();
10076 td.deregistered_at = Some(Utc::now());
10077 }
10078 }
10079 Ok(())
10080 }
10081
10082 fn create_ecs_service(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
10083 let props = &resource.properties;
10084 let service_name = props
10085 .get("ServiceName")
10086 .and_then(|v| v.as_str())
10087 .unwrap_or(&resource.logical_id)
10088 .to_string();
10089 let cluster_name = props
10091 .get("Cluster")
10092 .and_then(|v| v.as_str())
10093 .map(parse_ecs_cluster_name)
10094 .unwrap_or_else(|| "default".to_string());
10095 let task_definition_arn = props
10096 .get("TaskDefinition")
10097 .and_then(|v| v.as_str())
10098 .ok_or_else(|| "TaskDefinition is required".to_string())?
10099 .to_string();
10100 let desired_count = props
10101 .get("DesiredCount")
10102 .and_then(cfn_as_i64)
10103 .map(|n| n as i32)
10104 .unwrap_or(1);
10105 let launch_type = props
10106 .get("LaunchType")
10107 .and_then(|v| v.as_str())
10108 .unwrap_or("FARGATE")
10109 .to_string();
10110 let scheduling_strategy = props
10111 .get("SchedulingStrategy")
10112 .and_then(|v| v.as_str())
10113 .unwrap_or("REPLICA")
10114 .to_string();
10115 let deployment_controller = props
10116 .get("DeploymentController")
10117 .and_then(|v| v.get("Type"))
10118 .and_then(|v| v.as_str())
10119 .unwrap_or("ECS")
10120 .to_string();
10121 let load_balancers: Vec<serde_json::Value> = props
10122 .get("LoadBalancers")
10123 .and_then(|v| v.as_array())
10124 .cloned()
10125 .unwrap_or_default()
10126 .into_iter()
10127 .map(lowercase_first_keys)
10128 .collect();
10129 let service_registries: Vec<serde_json::Value> = props
10130 .get("ServiceRegistries")
10131 .and_then(|v| v.as_array())
10132 .cloned()
10133 .unwrap_or_default()
10134 .into_iter()
10135 .map(lowercase_first_keys)
10136 .collect();
10137 let placement_constraints: Vec<serde_json::Value> = props
10138 .get("PlacementConstraints")
10139 .and_then(|v| v.as_array())
10140 .cloned()
10141 .unwrap_or_default()
10142 .into_iter()
10143 .map(lowercase_first_keys)
10144 .collect();
10145 let placement_strategy: Vec<serde_json::Value> = props
10146 .get("PlacementStrategies")
10147 .and_then(|v| v.as_array())
10148 .cloned()
10149 .unwrap_or_default()
10150 .into_iter()
10151 .map(lowercase_first_keys)
10152 .collect();
10153 let network_configuration = props
10154 .get("NetworkConfiguration")
10155 .cloned()
10156 .map(lowercase_first_keys);
10157 let role_arn = props
10158 .get("Role")
10159 .and_then(|v| v.as_str())
10160 .map(|s| s.to_string());
10161 let platform_version = props
10162 .get("PlatformVersion")
10163 .and_then(|v| v.as_str())
10164 .map(|s| s.to_string());
10165 let health_check_grace_period_seconds = props
10166 .get("HealthCheckGracePeriodSeconds")
10167 .and_then(cfn_as_i64)
10168 .map(|n| n as i32);
10169 let enable_ecs_managed_tags = props
10170 .get("EnableECSManagedTags")
10171 .and_then(|v| v.as_bool())
10172 .unwrap_or(false);
10173 let enable_execute_command = props
10174 .get("EnableExecuteCommand")
10175 .and_then(|v| v.as_bool())
10176 .unwrap_or(false);
10177 let propagate_tags = props
10178 .get("PropagateTags")
10179 .and_then(|v| v.as_str())
10180 .map(|s| s.to_string());
10181 let capacity_provider_strategy: Vec<serde_json::Value> = props
10182 .get("CapacityProviderStrategy")
10183 .and_then(|v| v.as_array())
10184 .cloned()
10185 .unwrap_or_default()
10186 .into_iter()
10187 .map(lowercase_first_keys)
10188 .collect();
10189 let availability_zone_rebalancing = props
10190 .get("AvailabilityZoneRebalancing")
10191 .and_then(|v| v.as_str())
10192 .map(|s| s.to_string());
10193 let tags = parse_ecs_tags(props.get("Tags"));
10194
10195 let (family, revision) = parse_td_arn(&task_definition_arn);
10197
10198 let mut accounts = self.ecs_state.write();
10199 let state = accounts.get_or_create(&self.account_id);
10200 if !state.clusters.contains_key(&cluster_name) {
10201 return Err(format!(
10202 "Cluster {cluster_name} does not exist yet — retry once it has been provisioned"
10203 ));
10204 }
10205 let cluster_arn = state.clusters[&cluster_name].cluster_arn.clone();
10206 let service_arn = format!(
10207 "arn:aws:ecs:{}:{}:service/{}/{}",
10208 self.region, self.account_id, cluster_name, service_name
10209 );
10210 let key = format!("{cluster_name}/{service_name}");
10211 let service = EcsService {
10212 service_name: service_name.clone(),
10213 service_arn: service_arn.clone(),
10214 cluster_name: cluster_name.clone(),
10215 cluster_arn,
10216 task_definition_arn,
10217 family,
10218 revision,
10219 desired_count,
10220 running_count: 0,
10221 pending_count: 0,
10222 launch_type,
10223 status: "ACTIVE".to_string(),
10224 scheduling_strategy,
10225 deployment_controller,
10226 minimum_healthy_percent: props
10227 .get("DeploymentConfiguration")
10228 .and_then(|v| v.get("MinimumHealthyPercent"))
10229 .and_then(cfn_as_i64)
10230 .map(|n| n as i32),
10231 maximum_percent: props
10232 .get("DeploymentConfiguration")
10233 .and_then(|v| v.get("MaximumPercent"))
10234 .and_then(cfn_as_i64)
10235 .map(|n| n as i32),
10236 circuit_breaker: None,
10237 deployments: Vec::new(),
10238 load_balancers,
10239 service_registries,
10240 placement_constraints,
10241 placement_strategy,
10242 network_configuration,
10243 tags,
10244 created_at: Utc::now(),
10245 created_by: None,
10246 role_arn,
10247 platform_version,
10248 health_check_grace_period_seconds,
10249 enable_execute_command,
10250 enable_ecs_managed_tags,
10251 propagate_tags,
10252 capacity_provider_strategy,
10253 availability_zone_rebalancing,
10254 volume_configurations: Vec::new(),
10255 };
10256 state.services.insert(key.clone(), service);
10257 if let Some(c) = state.clusters.get_mut(&cluster_name) {
10258 c.active_services_count += 1;
10259 }
10260 Ok(ProvisionResult::new(service_arn.clone())
10261 .with("Name", service_name)
10262 .with("ServiceArn", service_arn))
10263 }
10264
10265 fn delete_ecs_service(&self, physical_id: &str) -> Result<(), String> {
10266 let Some((cluster, service)) = parse_service_arn(physical_id) else {
10268 return Ok(());
10269 };
10270 let key = format!("{cluster}/{service}");
10271 let mut accounts = self.ecs_state.write();
10272 let state = accounts.get_or_create(&self.account_id);
10273 if state.services.remove(&key).is_some() {
10274 if let Some(c) = state.clusters.get_mut(&cluster) {
10275 if c.active_services_count > 0 {
10276 c.active_services_count -= 1;
10277 }
10278 }
10279 }
10280 Ok(())
10281 }
10282
10283 fn create_ecs_capacity_provider(
10284 &self,
10285 resource: &ResourceDefinition,
10286 ) -> Result<ProvisionResult, String> {
10287 let props = &resource.properties;
10288 let name = props
10289 .get("Name")
10290 .and_then(|v| v.as_str())
10291 .unwrap_or(&resource.logical_id)
10292 .to_string();
10293 let arn = format!(
10294 "arn:aws:ecs:{}:{}:capacity-provider/{}",
10295 self.region, self.account_id, name
10296 );
10297 let cp = EcsCapacityProvider {
10298 name: name.clone(),
10299 arn: arn.clone(),
10300 status: "ACTIVE".to_string(),
10301 auto_scaling_group_provider: props.get("AutoScalingGroupProvider").cloned(),
10302 update_status: None,
10303 update_status_reason: None,
10304 created_at: Utc::now(),
10305 tags: parse_ecs_tags(props.get("Tags")),
10306 };
10307 let mut accounts = self.ecs_state.write();
10308 let state = accounts.get_or_create(&self.account_id);
10309 state.capacity_providers.insert(name.clone(), cp);
10310 Ok(ProvisionResult::new(name).with("Arn", arn))
10311 }
10312
10313 fn delete_ecs_capacity_provider(&self, physical_id: &str) -> Result<(), String> {
10314 let mut accounts = self.ecs_state.write();
10315 let state = accounts.get_or_create(&self.account_id);
10316 state.capacity_providers.remove(physical_id);
10317 Ok(())
10318 }
10319
10320 fn update_ecs_cluster(
10325 &self,
10326 existing: &StackResource,
10327 resource: &ResourceDefinition,
10328 ) -> Result<ProvisionResult, String> {
10329 let props = &resource.properties;
10330 let cluster_name = existing.physical_id.clone();
10331 let mut accounts = self.ecs_state.write();
10332 let state = accounts.get_or_create(&self.account_id);
10333 let cluster = state
10334 .clusters
10335 .get_mut(&cluster_name)
10336 .ok_or_else(|| format!("Cluster {cluster_name} no longer exists"))?;
10337 if let Some(settings) = props.get("ClusterSettings").and_then(|v| v.as_array()) {
10338 cluster.settings = settings.iter().cloned().map(lowercase_first_keys).collect();
10339 }
10340 if let Some(cfg) = props.get("Configuration") {
10341 cluster.configuration = Some(lowercase_first_keys(cfg.clone()));
10342 }
10343 if let Some(cps) = props.get("CapacityProviders").and_then(|v| v.as_array()) {
10344 cluster.capacity_providers = cps
10345 .iter()
10346 .filter_map(|v| v.as_str().map(|s| s.to_string()))
10347 .collect();
10348 }
10349 if let Some(strategy) = props
10350 .get("DefaultCapacityProviderStrategy")
10351 .and_then(|v| v.as_array())
10352 {
10353 cluster.default_capacity_provider_strategy =
10354 strategy.iter().cloned().map(lowercase_first_keys).collect();
10355 }
10356 if let Some(scd) = props.get("ServiceConnectDefaults") {
10357 cluster.service_connect_defaults = Some(lowercase_first_keys(scd.clone()));
10358 }
10359 if props.get("Tags").is_some() {
10360 cluster.tags = parse_ecs_tags(props.get("Tags"));
10361 }
10362 let cluster_arn = cluster.cluster_arn.clone();
10363 Ok(ProvisionResult::new(cluster_name).with("Arn", cluster_arn))
10364 }
10365
10366 fn update_ecs_service(
10371 &self,
10372 existing: &StackResource,
10373 resource: &ResourceDefinition,
10374 ) -> Result<ProvisionResult, String> {
10375 let props = &resource.properties;
10376 let service_arn = existing.physical_id.clone();
10377 let Some((cluster_name, service_name)) = parse_service_arn(&service_arn) else {
10378 return Err(format!("Cannot parse service ARN: {service_arn}"));
10379 };
10380 let key = format!("{cluster_name}/{service_name}");
10381 let mut accounts = self.ecs_state.write();
10382 let state = accounts.get_or_create(&self.account_id);
10383 let svc = state
10384 .services
10385 .get_mut(&key)
10386 .ok_or_else(|| format!("Service {service_arn} no longer exists"))?;
10387 if let Some(td) = props.get("TaskDefinition").and_then(|v| v.as_str()) {
10388 svc.task_definition_arn = td.to_string();
10389 let (family, revision) = parse_td_arn(td);
10390 svc.family = family;
10391 svc.revision = revision;
10392 }
10393 if let Some(n) = props.get("DesiredCount").and_then(cfn_as_i64) {
10394 svc.desired_count = n as i32;
10395 }
10396 if let Some(s) = props.get("LaunchType").and_then(|v| v.as_str()) {
10397 svc.launch_type = s.to_string();
10398 }
10399 if let Some(s) = props.get("PlatformVersion").and_then(|v| v.as_str()) {
10400 svc.platform_version = Some(s.to_string());
10401 }
10402 if let Some(n) = props
10403 .get("HealthCheckGracePeriodSeconds")
10404 .and_then(cfn_as_i64)
10405 {
10406 svc.health_check_grace_period_seconds = Some(n as i32);
10407 }
10408 if let Some(b) = props.get("EnableExecuteCommand").and_then(|v| v.as_bool()) {
10409 svc.enable_execute_command = b;
10410 }
10411 if let Some(b) = props.get("EnableECSManagedTags").and_then(|v| v.as_bool()) {
10412 svc.enable_ecs_managed_tags = b;
10413 }
10414 if let Some(s) = props.get("PropagateTags").and_then(|v| v.as_str()) {
10415 svc.propagate_tags = Some(s.to_string());
10416 }
10417 if let Some(s) = props
10418 .get("AvailabilityZoneRebalancing")
10419 .and_then(|v| v.as_str())
10420 {
10421 svc.availability_zone_rebalancing = Some(s.to_string());
10422 }
10423 if let Some(arr) = props
10424 .get("CapacityProviderStrategy")
10425 .and_then(|v| v.as_array())
10426 {
10427 svc.capacity_provider_strategy =
10428 arr.iter().cloned().map(lowercase_first_keys).collect();
10429 }
10430 if let Some(dc) = props.get("DeploymentConfiguration") {
10431 if let Some(n) = dc.get("MinimumHealthyPercent").and_then(cfn_as_i64) {
10432 svc.minimum_healthy_percent = Some(n as i32);
10433 }
10434 if let Some(n) = dc.get("MaximumPercent").and_then(cfn_as_i64) {
10435 svc.maximum_percent = Some(n as i32);
10436 }
10437 }
10438 if let Some(arr) = props.get("LoadBalancers").and_then(|v| v.as_array()) {
10439 svc.load_balancers = arr.iter().cloned().map(lowercase_first_keys).collect();
10440 }
10441 if let Some(arr) = props.get("ServiceRegistries").and_then(|v| v.as_array()) {
10442 svc.service_registries = arr.iter().cloned().map(lowercase_first_keys).collect();
10443 }
10444 if let Some(arr) = props.get("PlacementConstraints").and_then(|v| v.as_array()) {
10445 svc.placement_constraints = arr.iter().cloned().map(lowercase_first_keys).collect();
10446 }
10447 if let Some(arr) = props.get("PlacementStrategies").and_then(|v| v.as_array()) {
10448 svc.placement_strategy = arr.iter().cloned().map(lowercase_first_keys).collect();
10449 }
10450 if let Some(nc) = props.get("NetworkConfiguration") {
10451 svc.network_configuration = Some(lowercase_first_keys(nc.clone()));
10452 }
10453 if props.get("Tags").is_some() {
10454 svc.tags = parse_ecs_tags(props.get("Tags"));
10455 }
10456 let name = svc.service_name.clone();
10457 Ok(ProvisionResult::new(service_arn.clone())
10458 .with("Name", name)
10459 .with("ServiceArn", service_arn))
10460 }
10461
10462 fn update_ecs_task_definition(
10467 &self,
10468 _existing: &StackResource,
10469 resource: &ResourceDefinition,
10470 ) -> Result<ProvisionResult, String> {
10471 self.create_ecs_task_definition(resource)
10475 }
10476
10477 fn update_ecs_capacity_provider(
10480 &self,
10481 existing: &StackResource,
10482 resource: &ResourceDefinition,
10483 ) -> Result<ProvisionResult, String> {
10484 let props = &resource.properties;
10485 let name = existing.physical_id.clone();
10486 let mut accounts = self.ecs_state.write();
10487 let state = accounts.get_or_create(&self.account_id);
10488 let cp = state
10489 .capacity_providers
10490 .get_mut(&name)
10491 .ok_or_else(|| format!("CapacityProvider {name} no longer exists"))?;
10492 if props.get("AutoScalingGroupProvider").is_some() {
10493 cp.auto_scaling_group_provider = props.get("AutoScalingGroupProvider").cloned();
10494 }
10495 if props.get("Tags").is_some() {
10496 cp.tags = parse_ecs_tags(props.get("Tags"));
10497 }
10498 let arn = cp.arn.clone();
10499 Ok(ProvisionResult::new(name).with("Arn", arn))
10500 }
10501
10502 fn get_att_ecs_cluster(&self, physical_id: &str, attribute: &str) -> Option<String> {
10503 let mut accounts = self.ecs_state.write();
10504 let state = accounts.get_or_create(&self.account_id);
10505 let cluster = state.clusters.get(physical_id)?;
10506 match attribute {
10507 "Arn" => Some(cluster.cluster_arn.clone()),
10508 _ => None,
10509 }
10510 }
10511
10512 fn get_att_ecs_service(&self, physical_id: &str, attribute: &str) -> Option<String> {
10513 let (cluster, service) = parse_service_arn(physical_id)?;
10514 let key = format!("{cluster}/{service}");
10515 let mut accounts = self.ecs_state.write();
10516 let state = accounts.get_or_create(&self.account_id);
10517 let svc = state.services.get(&key)?;
10518 match attribute {
10519 "Name" => Some(svc.service_name.clone()),
10520 "ServiceArn" => Some(svc.service_arn.clone()),
10521 _ => None,
10522 }
10523 }
10524
10525 fn get_att_ecs_capacity_provider(&self, physical_id: &str, attribute: &str) -> Option<String> {
10526 let mut accounts = self.ecs_state.write();
10527 let state = accounts.get_or_create(&self.account_id);
10528 let cp = state.capacity_providers.get(physical_id)?;
10529 match attribute {
10530 "Arn" => Some(cp.arn.clone()),
10531 _ => None,
10532 }
10533 }
10534
10535 fn create_acm_certificate(
10538 &self,
10539 resource: &ResourceDefinition,
10540 ) -> Result<ProvisionResult, String> {
10541 let props = &resource.properties;
10542 let domain_name = props
10543 .get("DomainName")
10544 .and_then(|v| v.as_str())
10545 .ok_or_else(|| "DomainName is required".to_string())?
10546 .to_string();
10547 let sans: Vec<String> = props
10548 .get("SubjectAlternativeNames")
10549 .and_then(|v| v.as_array())
10550 .map(|arr| {
10551 arr.iter()
10552 .filter_map(|v| v.as_str().map(|s| s.to_string()))
10553 .collect()
10554 })
10555 .unwrap_or_default();
10556 let key_algorithm = props
10557 .get("KeyAlgorithm")
10558 .and_then(|v| v.as_str())
10559 .unwrap_or("RSA_2048")
10560 .to_string();
10561 let validation_method = props
10562 .get("ValidationMethod")
10563 .and_then(|v| v.as_str())
10564 .unwrap_or("DNS")
10565 .to_string();
10566 let ca_arn = props
10567 .get("CertificateAuthorityArn")
10568 .and_then(|v| v.as_str())
10569 .map(|s| s.to_string());
10570 let tags = parse_acm_tags(props.get("Tags"));
10571 let cert_transparency = props
10572 .get("CertificateTransparencyLoggingPreference")
10573 .and_then(|v| v.as_str())
10574 .unwrap_or("ENABLED")
10575 .to_string();
10576
10577 let arn = format!(
10579 "arn:aws:acm:{}:{}:certificate/{}",
10580 self.region,
10581 self.account_id,
10582 Uuid::new_v4()
10583 );
10584 let now = Utc::now();
10585
10586 let mut all_names = vec![domain_name.clone()];
10590 for s in &sans {
10591 if !all_names.contains(s) {
10592 all_names.push(s.clone());
10593 }
10594 }
10595 let (cert_pem, key_pem) = rcgen::generate_simple_self_signed(all_names.clone())
10596 .map(|c| (c.cert.pem(), c.key_pair.serialize_pem()))
10597 .map(|(c, k)| (Some(c), Some(k)))
10598 .unwrap_or((None, None));
10599
10600 let domain_validation: Vec<AcmDomainValidation> =
10608 synth_acm_domain_validation(&domain_name, &sans, &validation_method)
10609 .into_iter()
10610 .map(|mut dv| {
10611 dv.validation_status = "SUCCESS".to_string();
10612 dv
10613 })
10614 .collect();
10615 let renewal_summary = Some(AcmRenewalSummary {
10616 renewal_status: "PENDING_AUTO_RENEWAL".to_string(),
10617 domain_validation: domain_validation.clone(),
10618 renewal_status_reason: None,
10619 updated_at: now,
10620 });
10621 let cert = AcmStoredCertificate {
10622 arn: arn.clone(),
10623 domain_name: domain_name.clone(),
10624 subject_alternative_names: all_names,
10625 status: "ISSUED".to_string(),
10626 cert_type: "AMAZON_ISSUED".to_string(),
10627 certificate_pem: cert_pem,
10628 certificate_chain_pem: None,
10629 private_key_pem: key_pem,
10630 idempotency_token: None,
10631 serial: format!("{:032x}", Uuid::new_v4().as_u128()),
10632 subject: format!("CN={domain_name}"),
10633 issuer: "Amazon".to_string(),
10634 key_algorithm,
10635 signature_algorithm: "SHA256WITHRSA".to_string(),
10636 created_at: now,
10637 issued_at: Some(now),
10638 imported_at: None,
10639 revoked_at: None,
10640 revocation_reason: None,
10641 not_before: now,
10642 not_after: now + chrono::Duration::days(395),
10644 validation_method: Some(validation_method.clone()),
10645 domain_validation,
10646 options: AcmCertificateOptions {
10647 certificate_transparency_logging_preference: cert_transparency,
10648 export: "DISABLED".to_string(),
10649 },
10650 renewal_eligibility: "ELIGIBLE".to_string(),
10651 managed_by: None,
10652 certificate_authority_arn: ca_arn,
10653 tags,
10654 in_use_by: Vec::new(),
10655 describe_read_count: 0,
10656 failure_reason: None,
10657 renewal_summary,
10658 };
10659
10660 let mut accounts = self.acm_state.write();
10661 let account = accounts
10662 .accounts
10663 .entry(self.account_id.clone())
10664 .or_default();
10665 account.certificates.insert(arn.clone(), cert);
10666
10667 Ok(ProvisionResult::new(arn))
10668 }
10669
10670 fn delete_acm_certificate(&self, physical_id: &str) -> Result<(), String> {
10671 let mut accounts = self.acm_state.write();
10672 if let Some(account) = accounts.accounts.get_mut(&self.account_id) {
10673 account.certificates.remove(physical_id);
10674 }
10675 Ok(())
10676 }
10677
10678 fn create_acm_account(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
10682 let days = resource
10683 .properties
10684 .get("ExpiryEventsConfiguration")
10685 .and_then(|v| v.get("DaysBeforeExpiry"))
10686 .and_then(|v| v.as_i64())
10687 .map(|n| n as i32);
10688 let mut accounts = self.acm_state.write();
10689 let account = accounts
10690 .accounts
10691 .entry(self.account_id.clone())
10692 .or_default();
10693 account.account_config.expiry_events_days_before_expiry = days;
10694 Ok(ProvisionResult::new(format!(
10695 "acm-account-{}",
10696 self.account_id
10697 )))
10698 }
10699
10700 fn delete_acm_account(&self) -> Result<(), String> {
10704 let mut accounts = self.acm_state.write();
10705 if let Some(account) = accounts.accounts.get_mut(&self.account_id) {
10706 account.account_config.expiry_events_days_before_expiry = None;
10707 }
10708 Ok(())
10709 }
10710
10711 fn create_ec_parameter_group(
10714 &self,
10715 resource: &ResourceDefinition,
10716 ) -> Result<ProvisionResult, String> {
10717 let props = &resource.properties;
10718 let name = props
10719 .get("CacheParameterGroupName")
10720 .and_then(|v| v.as_str())
10721 .unwrap_or(&resource.logical_id)
10722 .to_string();
10723 let family = props
10724 .get("CacheParameterGroupFamily")
10725 .and_then(|v| v.as_str())
10726 .unwrap_or("redis7")
10727 .to_string();
10728 let description = props
10729 .get("Description")
10730 .and_then(|v| v.as_str())
10731 .unwrap_or("")
10732 .to_string();
10733 let arn = format!(
10734 "arn:aws:elasticache:{}:{}:parametergroup:{}",
10735 self.region, self.account_id, name
10736 );
10737 let group = CacheParameterGroup {
10738 cache_parameter_group_name: name.clone(),
10739 cache_parameter_group_family: family,
10740 description,
10741 is_global: false,
10742 arn: arn.clone(),
10743 };
10744 let mut accounts = self.elasticache_state.write();
10745 let state = accounts.get_or_create(&self.account_id);
10746 state
10748 .parameter_groups
10749 .retain(|p| p.cache_parameter_group_name != name);
10750 state.parameter_groups.push(group);
10751 Ok(ProvisionResult::new(name).with("Arn", arn))
10752 }
10753
10754 fn delete_ec_parameter_group(&self, physical_id: &str) -> Result<(), String> {
10755 let mut accounts = self.elasticache_state.write();
10756 let state = accounts.get_or_create(&self.account_id);
10757 state
10758 .parameter_groups
10759 .retain(|p| p.cache_parameter_group_name != physical_id);
10760 Ok(())
10761 }
10762
10763 fn create_ec_subnet_group(
10764 &self,
10765 resource: &ResourceDefinition,
10766 ) -> Result<ProvisionResult, String> {
10767 let props = &resource.properties;
10768 let name = props
10769 .get("CacheSubnetGroupName")
10770 .and_then(|v| v.as_str())
10771 .unwrap_or(&resource.logical_id)
10772 .to_string();
10773 let description = props
10774 .get("Description")
10775 .and_then(|v| v.as_str())
10776 .unwrap_or("")
10777 .to_string();
10778 let subnet_ids: Vec<String> = props
10779 .get("SubnetIds")
10780 .and_then(|v| v.as_array())
10781 .map(|arr| {
10782 arr.iter()
10783 .filter_map(|v| v.as_str().map(|s| s.to_string()))
10784 .collect()
10785 })
10786 .unwrap_or_default();
10787 let arn = format!(
10788 "arn:aws:elasticache:{}:{}:subnetgroup:{}",
10789 self.region, self.account_id, name
10790 );
10791 let group = CacheSubnetGroup {
10792 cache_subnet_group_name: name.clone(),
10793 cache_subnet_group_description: description,
10794 vpc_id: String::new(),
10795 subnet_ids,
10796 arn: arn.clone(),
10797 };
10798 let mut accounts = self.elasticache_state.write();
10799 let state = accounts.get_or_create(&self.account_id);
10800 state.subnet_groups.insert(name.clone(), group);
10801 Ok(ProvisionResult::new(name).with("Arn", arn))
10802 }
10803
10804 fn delete_ec_subnet_group(&self, physical_id: &str) -> Result<(), String> {
10805 let mut accounts = self.elasticache_state.write();
10806 let state = accounts.get_or_create(&self.account_id);
10807 state.subnet_groups.remove(physical_id);
10808 Ok(())
10809 }
10810
10811 fn create_ec_security_group(
10812 &self,
10813 resource: &ResourceDefinition,
10814 ) -> Result<ProvisionResult, String> {
10815 let props = &resource.properties;
10816 let name = props
10817 .get("CacheSecurityGroupName")
10818 .and_then(|v| v.as_str())
10819 .unwrap_or(&resource.logical_id)
10820 .to_string();
10821 let description = props
10822 .get("Description")
10823 .and_then(|v| v.as_str())
10824 .unwrap_or("")
10825 .to_string();
10826 let arn = format!(
10827 "arn:aws:elasticache:{}:{}:securitygroup:{}",
10828 self.region, self.account_id, name
10829 );
10830 let group = CacheSecurityGroup {
10831 cache_security_group_name: name.clone(),
10832 description,
10833 owner_id: self.account_id.clone(),
10834 arn: arn.clone(),
10835 ec2_security_groups: Vec::new(),
10836 };
10837 let mut accounts = self.elasticache_state.write();
10838 let state = accounts.get_or_create(&self.account_id);
10839 state.security_groups.insert(name.clone(), group);
10840 Ok(ProvisionResult::new(name).with("Arn", arn))
10841 }
10842
10843 fn delete_ec_security_group(&self, physical_id: &str) -> Result<(), String> {
10844 let mut accounts = self.elasticache_state.write();
10845 let state = accounts.get_or_create(&self.account_id);
10846 state.security_groups.remove(physical_id);
10847 Ok(())
10848 }
10849
10850 fn create_ec_user(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
10851 let props = &resource.properties;
10852 let user_id = props
10853 .get("UserId")
10854 .and_then(|v| v.as_str())
10855 .unwrap_or(&resource.logical_id)
10856 .to_string();
10857 let user_name = props
10858 .get("UserName")
10859 .and_then(|v| v.as_str())
10860 .unwrap_or(&user_id)
10861 .to_string();
10862 let engine = props
10863 .get("Engine")
10864 .and_then(|v| v.as_str())
10865 .unwrap_or("redis")
10866 .to_string();
10867 let access_string = props
10868 .get("AccessString")
10869 .and_then(|v| v.as_str())
10870 .unwrap_or("on ~* +@all")
10871 .to_string();
10872 let authentication_type = props
10873 .get("AuthenticationMode")
10874 .and_then(|v| v.get("Type"))
10875 .and_then(|v| v.as_str())
10876 .unwrap_or("no-password-required")
10877 .to_string();
10878 let arn = format!(
10879 "arn:aws:elasticache:{}:{}:user:{}",
10880 self.region, self.account_id, user_id
10881 );
10882 let user = EcUser {
10883 user_id: user_id.clone(),
10884 user_name,
10885 engine,
10886 access_string,
10887 status: "active".to_string(),
10888 authentication_type,
10889 password_count: 0,
10890 arn: arn.clone(),
10891 minimum_engine_version: "6.0".to_string(),
10892 user_group_ids: Vec::new(),
10893 };
10894 let mut accounts = self.elasticache_state.write();
10895 let state = accounts.get_or_create(&self.account_id);
10896 state.users.insert(user_id.clone(), user);
10897 Ok(ProvisionResult::new(user_id).with("Arn", arn))
10898 }
10899
10900 fn delete_ec_user(&self, physical_id: &str) -> Result<(), String> {
10901 let mut accounts = self.elasticache_state.write();
10902 let state = accounts.get_or_create(&self.account_id);
10903 state.users.remove(physical_id);
10904 Ok(())
10905 }
10906
10907 fn create_ec_user_group(
10908 &self,
10909 resource: &ResourceDefinition,
10910 ) -> Result<ProvisionResult, String> {
10911 let props = &resource.properties;
10912 let user_group_id = props
10913 .get("UserGroupId")
10914 .and_then(|v| v.as_str())
10915 .unwrap_or(&resource.logical_id)
10916 .to_string();
10917 let engine = props
10918 .get("Engine")
10919 .and_then(|v| v.as_str())
10920 .unwrap_or("redis")
10921 .to_string();
10922 let user_ids: Vec<String> = props
10923 .get("UserIds")
10924 .and_then(|v| v.as_array())
10925 .map(|arr| {
10926 arr.iter()
10927 .filter_map(|v| v.as_str().map(|s| s.to_string()))
10928 .collect()
10929 })
10930 .unwrap_or_default();
10931 let arn = format!(
10932 "arn:aws:elasticache:{}:{}:usergroup:{}",
10933 self.region, self.account_id, user_group_id
10934 );
10935 let group = EcUserGroup {
10936 user_group_id: user_group_id.clone(),
10937 engine,
10938 status: "active".to_string(),
10939 user_ids,
10940 arn: arn.clone(),
10941 minimum_engine_version: "6.0".to_string(),
10942 pending_changes: None,
10943 replication_groups: Vec::new(),
10944 };
10945 let mut accounts = self.elasticache_state.write();
10946 let state = accounts.get_or_create(&self.account_id);
10947 state.user_groups.insert(user_group_id.clone(), group);
10948 Ok(ProvisionResult::new(user_group_id).with("Arn", arn))
10949 }
10950
10951 fn delete_ec_user_group(&self, physical_id: &str) -> Result<(), String> {
10952 let mut accounts = self.elasticache_state.write();
10953 let state = accounts.get_or_create(&self.account_id);
10954 state.user_groups.remove(physical_id);
10955 Ok(())
10956 }
10957
10958 fn create_ec_cache_cluster(
10959 &self,
10960 resource: &ResourceDefinition,
10961 ) -> Result<ProvisionResult, String> {
10962 let props = &resource.properties;
10963 let id = props
10964 .get("ClusterName")
10965 .and_then(|v| v.as_str())
10966 .map(String::from)
10967 .unwrap_or_else(|| format!("cfn-cc-{}", resource.logical_id.to_lowercase()));
10968 let cache_node_type = props
10969 .get("CacheNodeType")
10970 .and_then(|v| v.as_str())
10971 .unwrap_or("cache.t4g.micro")
10972 .to_string();
10973 let engine = props
10974 .get("Engine")
10975 .and_then(|v| v.as_str())
10976 .unwrap_or("redis")
10977 .to_string();
10978 let engine_version = props
10979 .get("EngineVersion")
10980 .and_then(|v| v.as_str())
10981 .unwrap_or("7.1")
10982 .to_string();
10983 let num_cache_nodes = props
10984 .get("NumCacheNodes")
10985 .and_then(|v| v.as_i64())
10986 .map(|n| n as i32)
10987 .unwrap_or(1);
10988 let preferred_az = props
10989 .get("PreferredAvailabilityZone")
10990 .and_then(|v| v.as_str())
10991 .unwrap_or("us-east-1a")
10992 .to_string();
10993 let cache_subnet_group_name = props
10994 .get("CacheSubnetGroupName")
10995 .and_then(|v| v.as_str())
10996 .map(String::from);
10997 let auto_minor_version_upgrade = props
10998 .get("AutoMinorVersionUpgrade")
10999 .and_then(|v| v.as_bool())
11000 .unwrap_or(true);
11001 let default_port = if engine == "memcached" { 11211 } else { 6379 };
11002 let port = props
11003 .get("Port")
11004 .and_then(|v| v.as_i64())
11005 .map(|n| n as u16)
11006 .unwrap_or(default_port);
11007 let cache_parameter_group_name = props
11008 .get("CacheParameterGroupName")
11009 .and_then(|v| v.as_str())
11010 .map(String::from);
11011 let security_group_ids: Vec<String> = props
11012 .get("VpcSecurityGroupIds")
11013 .and_then(|v| v.as_array())
11014 .map(|arr| {
11015 arr.iter()
11016 .filter_map(|v| v.as_str().map(String::from))
11017 .collect()
11018 })
11019 .unwrap_or_default();
11020 let cache_security_group_names: Vec<String> = props
11021 .get("CacheSecurityGroupNames")
11022 .and_then(|v| v.as_array())
11023 .map(|arr| {
11024 arr.iter()
11025 .filter_map(|v| v.as_str().map(String::from))
11026 .collect()
11027 })
11028 .unwrap_or_default();
11029 let preferred_availability_zones: Vec<String> = props
11030 .get("PreferredAvailabilityZones")
11031 .and_then(|v| v.as_array())
11032 .map(|arr| {
11033 arr.iter()
11034 .filter_map(|v| v.as_str().map(String::from))
11035 .collect()
11036 })
11037 .unwrap_or_default();
11038 let snapshot_arns: Vec<String> = props
11039 .get("SnapshotArns")
11040 .and_then(|v| v.as_array())
11041 .map(|arr| {
11042 arr.iter()
11043 .filter_map(|v| v.as_str().map(String::from))
11044 .collect()
11045 })
11046 .unwrap_or_default();
11047 let snapshot_name = props
11048 .get("SnapshotName")
11049 .and_then(|v| v.as_str())
11050 .map(String::from);
11051 let snapshot_retention_limit = props
11052 .get("SnapshotRetentionLimit")
11053 .and_then(|v| v.as_i64())
11054 .map(|n| n as i32)
11055 .unwrap_or(0);
11056 let snapshot_window = props
11057 .get("SnapshotWindow")
11058 .and_then(|v| v.as_str())
11059 .map(String::from);
11060 let preferred_maintenance_window = props
11061 .get("PreferredMaintenanceWindow")
11062 .and_then(|v| v.as_str())
11063 .map(String::from);
11064 let notification_topic_arn = props
11065 .get("NotificationTopicArn")
11066 .and_then(|v| v.as_str())
11067 .map(String::from);
11068 let transit_encryption_enabled = props
11069 .get("TransitEncryptionEnabled")
11070 .and_then(|v| v.as_bool())
11071 .unwrap_or(false);
11072 let auth_token = props
11073 .get("AuthToken")
11074 .and_then(|v| v.as_str())
11075 .filter(|s| !s.is_empty())
11076 .map(String::from);
11077 let auth_token_enabled = auth_token.is_some();
11078 let network_type = props
11079 .get("NetworkType")
11080 .and_then(|v| v.as_str())
11081 .map(String::from)
11082 .or_else(|| Some("ipv4".to_string()));
11083 let ip_discovery = props
11084 .get("IpDiscovery")
11085 .and_then(|v| v.as_str())
11086 .map(String::from)
11087 .or_else(|| Some("ipv4".to_string()));
11088 let az_mode = props
11089 .get("AZMode")
11090 .and_then(|v| v.as_str())
11091 .map(String::from)
11092 .or_else(|| Some("single-az".to_string()));
11093 let outpost_mode = props
11094 .get("OutpostMode")
11095 .and_then(|v| v.as_str())
11096 .map(String::from);
11097 let preferred_outpost_arn = props
11098 .get("PreferredOutpostArn")
11099 .and_then(|v| v.as_str())
11100 .map(String::from);
11101
11102 let mut accounts = self.elasticache_state.write();
11103 let state = accounts.get_or_create(&self.account_id);
11104 let arn = format!(
11105 "arn:aws:elasticache:{}:{}:cluster:{}",
11106 state.region, state.account_id, id
11107 );
11108 let endpoint_address = format!("{id}.fakecloud.{}.cache.amazonaws.com", state.region);
11109 let cluster = EcCacheCluster {
11110 cache_cluster_id: id.clone(),
11111 cache_node_type,
11112 engine,
11113 engine_version,
11114 cache_cluster_status: "available".to_string(),
11115 num_cache_nodes,
11116 preferred_availability_zone: preferred_az,
11117 cache_subnet_group_name,
11118 auto_minor_version_upgrade,
11119 arn: arn.clone(),
11120 created_at: Utc::now().to_rfc3339(),
11121 endpoint_address: endpoint_address.clone(),
11122 endpoint_port: port,
11123 container_id: String::new(),
11124 host_port: 0,
11125 replication_group_id: None,
11126 cache_parameter_group_name,
11127 security_group_ids,
11128 log_delivery_configurations: Vec::new(),
11129 transit_encryption_enabled,
11130 at_rest_encryption_enabled: false,
11131 auth_token_enabled,
11132 port,
11133 preferred_maintenance_window,
11134 preferred_availability_zones,
11135 notification_topic_arn,
11136 cache_security_group_names,
11137 snapshot_arns,
11138 snapshot_name,
11139 snapshot_retention_limit,
11140 snapshot_window,
11141 outpost_mode,
11142 preferred_outpost_arn,
11143 network_type,
11144 ip_discovery,
11145 az_mode,
11146 auth_token,
11147 kms_key_id: None,
11148 transit_encryption_mode: None,
11149 data_tiering_enabled: None,
11150 cluster_mode: None,
11151 preferred_outpost_arns: Vec::new(),
11152 };
11153 state.cache_clusters.insert(id.clone(), cluster);
11154 Ok(ProvisionResult::new(id.clone())
11155 .with("Arn", arn)
11156 .with("RedisEndpoint.Address", endpoint_address.clone())
11157 .with("RedisEndpoint.Port", port.to_string())
11158 .with("ConfigurationEndpoint.Address", endpoint_address)
11159 .with("ConfigurationEndpoint.Port", port.to_string()))
11160 }
11161
11162 fn delete_ec_cache_cluster(&self, physical_id: &str) -> Result<(), String> {
11163 let mut accounts = self.elasticache_state.write();
11164 let state = accounts.get_or_create(&self.account_id);
11165 state.cache_clusters.remove(physical_id);
11166 Ok(())
11167 }
11168
11169 fn create_ec_replication_group(
11170 &self,
11171 resource: &ResourceDefinition,
11172 ) -> Result<ProvisionResult, String> {
11173 let props = &resource.properties;
11174 let id = props
11175 .get("ReplicationGroupId")
11176 .and_then(|v| v.as_str())
11177 .map(String::from)
11178 .unwrap_or_else(|| format!("cfn-rg-{}", resource.logical_id.to_lowercase()));
11179 let description = props
11180 .get("ReplicationGroupDescription")
11181 .and_then(|v| v.as_str())
11182 .unwrap_or("CFN-provisioned replication group")
11183 .to_string();
11184 let cache_node_type = props
11185 .get("CacheNodeType")
11186 .and_then(|v| v.as_str())
11187 .unwrap_or("cache.t4g.micro")
11188 .to_string();
11189 let engine = props
11190 .get("Engine")
11191 .and_then(|v| v.as_str())
11192 .unwrap_or("redis")
11193 .to_string();
11194 let engine_version = props
11195 .get("EngineVersion")
11196 .and_then(|v| v.as_str())
11197 .unwrap_or("7.1")
11198 .to_string();
11199 let num_cache_clusters = props
11200 .get("NumCacheClusters")
11201 .and_then(|v| v.as_i64())
11202 .map(|n| n as i32)
11203 .unwrap_or(1);
11204 let num_node_groups = props
11205 .get("NumNodeGroups")
11206 .and_then(|v| v.as_i64())
11207 .map(|n| n as i32)
11208 .unwrap_or(0);
11209 let replicas_per_node_group = props
11210 .get("ReplicasPerNodeGroup")
11211 .and_then(|v| v.as_i64())
11212 .map(|n| n as i32);
11213 let automatic_failover_enabled = props
11214 .get("AutomaticFailoverEnabled")
11215 .and_then(|v| v.as_bool())
11216 .unwrap_or(false);
11217 let multi_az_enabled = props
11218 .get("MultiAZEnabled")
11219 .and_then(|v| v.as_bool())
11220 .unwrap_or(false);
11221 let transit_encryption_enabled = props
11222 .get("TransitEncryptionEnabled")
11223 .and_then(|v| v.as_bool())
11224 .unwrap_or(false);
11225 let at_rest_encryption_enabled = props
11226 .get("AtRestEncryptionEnabled")
11227 .and_then(|v| v.as_bool())
11228 .unwrap_or(false);
11229 let kms_key_id = props
11230 .get("KmsKeyId")
11231 .and_then(|v| v.as_str())
11232 .map(String::from);
11233 let auth_token_enabled = props
11234 .get("AuthToken")
11235 .and_then(|v| v.as_str())
11236 .filter(|s| !s.is_empty())
11237 .is_some();
11238 let user_group_ids: Vec<String> = props
11239 .get("UserGroupIds")
11240 .and_then(|v| v.as_array())
11241 .map(|arr| {
11242 arr.iter()
11243 .filter_map(|v| v.as_str().map(String::from))
11244 .collect()
11245 })
11246 .unwrap_or_default();
11247 let snapshot_retention_limit = props
11248 .get("SnapshotRetentionLimit")
11249 .and_then(|v| v.as_i64())
11250 .map(|n| n as i32)
11251 .unwrap_or(0);
11252 let snapshot_window = props
11253 .get("SnapshotWindow")
11254 .and_then(|v| v.as_str())
11255 .unwrap_or("00:00-01:00")
11256 .to_string();
11257 let port = props
11258 .get("Port")
11259 .and_then(|v| v.as_i64())
11260 .map(|n| n as u16)
11261 .unwrap_or(6379);
11262 let cluster_enabled = num_node_groups > 1
11263 || props
11264 .get("ClusterEnabled")
11265 .and_then(|v| v.as_bool())
11266 .unwrap_or(false);
11267
11268 let mut accounts = self.elasticache_state.write();
11269 let state = accounts.get_or_create(&self.account_id);
11270 let arn = format!(
11271 "arn:aws:elasticache:{}:{}:replicationgroup:{}",
11272 state.region, state.account_id, id
11273 );
11274 let endpoint_address = format!(
11275 "{id}.fakecloud.ng.0001.{}.cache.amazonaws.com",
11276 state.region
11277 );
11278 let configuration_endpoint = if cluster_enabled {
11279 Some(format!(
11280 "{id}.fakecloud.cfg.{}.cache.amazonaws.com",
11281 state.region
11282 ))
11283 } else {
11284 None
11285 };
11286
11287 let group = EcReplicationGroup {
11288 replication_group_id: id.clone(),
11289 description,
11290 global_replication_group_id: None,
11291 global_replication_group_role: None,
11292 status: "available".to_string(),
11293 cache_node_type,
11294 engine,
11295 engine_version,
11296 num_cache_clusters,
11297 automatic_failover_enabled,
11298 endpoint_address: endpoint_address.clone(),
11299 endpoint_port: port,
11300 arn: arn.clone(),
11301 created_at: Utc::now().to_rfc3339(),
11302 container_id: String::new(),
11303 host_port: 0,
11304 member_clusters: Vec::new(),
11305 snapshot_retention_limit,
11306 snapshot_window,
11307 transit_encryption_enabled,
11308 at_rest_encryption_enabled,
11309 cluster_enabled,
11310 kms_key_id,
11311 auth_token_enabled,
11312 user_group_ids,
11313 multi_az_enabled,
11314 log_delivery_configurations: Vec::new(),
11315 data_tiering: props
11316 .get("DataTieringEnabled")
11317 .and_then(|v| v.as_bool())
11318 .map(|b| if b { "enabled" } else { "disabled" }.to_string()),
11319 ip_discovery: props
11320 .get("IpDiscovery")
11321 .and_then(|v| v.as_str())
11322 .map(String::from),
11323 network_type: props
11324 .get("NetworkType")
11325 .and_then(|v| v.as_str())
11326 .map(String::from),
11327 transit_encryption_mode: props
11328 .get("TransitEncryptionMode")
11329 .and_then(|v| v.as_str())
11330 .map(String::from),
11331 num_node_groups,
11332 configuration_endpoint_address: configuration_endpoint.clone(),
11333 configuration_endpoint_port: configuration_endpoint.as_ref().map(|_| port),
11334 replicas_per_node_group,
11335 auth_token: props
11336 .get("AuthToken")
11337 .and_then(|v| v.as_str())
11338 .filter(|s| !s.is_empty())
11339 .map(String::from),
11340 port,
11341 notification_topic_arn: props
11342 .get("NotificationTopicArn")
11343 .and_then(|v| v.as_str())
11344 .map(String::from),
11345 cluster_mode: props
11346 .get("ClusterMode")
11347 .and_then(|v| v.as_str())
11348 .map(String::from),
11349 data_tiering_enabled: props.get("DataTieringEnabled").and_then(|v| v.as_bool()),
11350 notification_topic_status: None,
11351 cache_parameter_group_name: props
11352 .get("CacheParameterGroupName")
11353 .and_then(|v| v.as_str())
11354 .map(String::from),
11355 cache_subnet_group_name: props
11356 .get("CacheSubnetGroupName")
11357 .and_then(|v| v.as_str())
11358 .map(String::from),
11359 security_group_ids: props
11360 .get("SecurityGroupIds")
11361 .and_then(|v| v.as_array())
11362 .map(|arr| {
11363 arr.iter()
11364 .filter_map(|v| v.as_str().map(String::from))
11365 .collect()
11366 })
11367 .unwrap_or_default(),
11368 preferred_maintenance_window: props
11369 .get("PreferredMaintenanceWindow")
11370 .and_then(|v| v.as_str())
11371 .map(String::from),
11372 snapshot_name: props
11373 .get("SnapshotName")
11374 .and_then(|v| v.as_str())
11375 .map(String::from),
11376 snapshot_arns: props
11377 .get("SnapshotArns")
11378 .and_then(|v| v.as_array())
11379 .map(|arr| {
11380 arr.iter()
11381 .filter_map(|v| v.as_str().map(String::from))
11382 .collect()
11383 })
11384 .unwrap_or_default(),
11385 auto_minor_version_upgrade: props
11386 .get("AutoMinorVersionUpgrade")
11387 .and_then(|v| v.as_bool())
11388 .unwrap_or(true),
11389 };
11390 state.replication_groups.insert(id.clone(), group);
11391
11392 let mut result = ProvisionResult::new(id.clone())
11393 .with("Arn", arn)
11394 .with("PrimaryEndPoint.Address", endpoint_address.clone())
11395 .with("PrimaryEndPoint.Port", port.to_string())
11396 .with("ReadEndPoint.Addresses", endpoint_address.clone())
11397 .with("ReadEndPoint.Ports", port.to_string());
11398 if let Some(cfg) = configuration_endpoint {
11399 result = result
11400 .with("ConfigurationEndPoint.Address", cfg)
11401 .with("ConfigurationEndPoint.Port", port.to_string());
11402 }
11403 Ok(result)
11404 }
11405
11406 fn delete_ec_replication_group(&self, physical_id: &str) -> Result<(), String> {
11407 let mut accounts = self.elasticache_state.write();
11408 let state = accounts.get_or_create(&self.account_id);
11409 state.replication_groups.remove(physical_id);
11410 Ok(())
11411 }
11412
11413 fn create_route53_hosted_zone(
11416 &self,
11417 resource: &ResourceDefinition,
11418 ) -> Result<ProvisionResult, String> {
11419 let props = &resource.properties;
11420 let name = props
11421 .get("Name")
11422 .and_then(|v| v.as_str())
11423 .ok_or("Name is required")?
11424 .to_string();
11425 let normalized_name = if name.ends_with('.') {
11426 name.clone()
11427 } else {
11428 format!("{name}.")
11429 };
11430 let comment = props
11431 .get("HostedZoneConfig")
11432 .and_then(|v| v.get("Comment"))
11433 .and_then(|v| v.as_str())
11434 .map(|s| s.to_string());
11435 let private_zone = props
11436 .get("VPCs")
11437 .and_then(|v| v.as_array())
11438 .map(|a| !a.is_empty())
11439 .unwrap_or(false);
11440 let vpcs: Vec<fakecloud_route53::model::VPC> = props
11441 .get("VPCs")
11442 .and_then(|v| v.as_array())
11443 .map(|arr| {
11444 arr.iter()
11445 .map(|vpc| fakecloud_route53::model::VPC {
11446 vpc_id: vpc.get("VPCId").and_then(|v| v.as_str()).map(String::from),
11447 vpc_region: vpc
11448 .get("VPCRegion")
11449 .and_then(|v| v.as_str())
11450 .map(String::from),
11451 })
11452 .collect()
11453 })
11454 .unwrap_or_default();
11455
11456 let id = format!(
11457 "Z{}",
11458 Uuid::new_v4().simple().to_string()[..14].to_uppercase()
11459 );
11460 let name_servers = (1..=4)
11461 .map(|i| format!("ns-{}.awsdns-{:02}.com", 100 + i, i))
11462 .collect::<Vec<_>>();
11463
11464 let zone = StoredHostedZone {
11465 id: id.clone(),
11466 name: normalized_name,
11467 caller_reference: format!("cfn-{}", resource.logical_id),
11468 comment,
11469 private_zone,
11470 features: Some(HostedZoneFeatures::default()),
11471 vpcs,
11472 delegation_set_id: None,
11473 name_servers: name_servers.clone(),
11474 created_time: Utc::now(),
11475 resource_record_sets: Vec::new(),
11476 };
11477
11478 let mut accounts = self.route53_state.write();
11479 let state = accounts.entry("000000000000");
11482 state.hosted_zones.insert(id.clone(), zone);
11483
11484 let mut result = ProvisionResult::new(id.clone()).with("Id", id);
11485 for (i, ns) in name_servers.iter().enumerate() {
11486 result = result.with(&format!("NameServers.{i}"), ns.clone());
11487 }
11488 result = result.with("NameServers", name_servers.join(","));
11489 Ok(result)
11490 }
11491
11492 fn delete_route53_hosted_zone(&self, physical_id: &str) -> Result<(), String> {
11493 let mut accounts = self.route53_state.write();
11494 let state = accounts.entry("000000000000");
11497 state.hosted_zones.remove(physical_id);
11498 Ok(())
11499 }
11500
11501 fn create_route53_record_set(
11502 &self,
11503 resource: &ResourceDefinition,
11504 ) -> Result<ProvisionResult, String> {
11505 let props = &resource.properties;
11506 let zone_id = props
11507 .get("HostedZoneId")
11508 .and_then(|v| v.as_str())
11509 .ok_or_else(|| {
11510 "HostedZoneId is required (HostedZoneName lookups not supported)".to_string()
11511 })?
11512 .to_string();
11513 let name = props
11514 .get("Name")
11515 .and_then(|v| v.as_str())
11516 .ok_or("Name is required")?
11517 .to_string();
11518 let normalized_name = if name.ends_with('.') {
11519 name.clone()
11520 } else {
11521 format!("{name}.")
11522 };
11523 let record_type = props
11524 .get("Type")
11525 .and_then(|v| v.as_str())
11526 .ok_or("Type is required")?
11527 .to_string();
11528 let ttl = props.get("TTL").and_then(|v| {
11529 v.as_str()
11530 .and_then(|s| s.parse::<i64>().ok())
11531 .or_else(|| v.as_i64())
11532 });
11533 let resource_records = props
11534 .get("ResourceRecords")
11535 .and_then(|v| v.as_array())
11536 .map(|arr| {
11537 let recs: Vec<fakecloud_route53::model::ResourceRecord> = arr
11538 .iter()
11539 .filter_map(|v| {
11540 v.as_str()
11541 .map(|s| fakecloud_route53::model::ResourceRecord {
11542 value: s.to_string(),
11543 })
11544 })
11545 .collect();
11546 fakecloud_route53::model::ResourceRecords {
11547 resource_record: recs,
11548 }
11549 });
11550 let alias_target =
11551 props
11552 .get("AliasTarget")
11553 .map(|v| fakecloud_route53::model::AliasTarget {
11554 hosted_zone_id: v
11555 .get("HostedZoneId")
11556 .and_then(|x| x.as_str())
11557 .unwrap_or("")
11558 .to_string(),
11559 dns_name: v
11560 .get("DNSName")
11561 .and_then(|x| x.as_str())
11562 .unwrap_or("")
11563 .to_string(),
11564 evaluate_target_health: v
11565 .get("EvaluateTargetHealth")
11566 .and_then(|x| x.as_bool())
11567 .unwrap_or(false),
11568 });
11569 let set_identifier = props
11570 .get("SetIdentifier")
11571 .and_then(|v| v.as_str())
11572 .map(String::from);
11573 let weight = props.get("Weight").and_then(|v| {
11574 v.as_i64()
11575 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11576 });
11577 let region = props
11578 .get("Region")
11579 .and_then(|v| v.as_str())
11580 .map(String::from);
11581 let failover = props
11582 .get("Failover")
11583 .and_then(|v| v.as_str())
11584 .map(String::from);
11585 let multi_value_answer = props.get("MultiValueAnswer").and_then(|v| v.as_bool());
11586 let health_check_id = props
11587 .get("HealthCheckId")
11588 .and_then(|v| v.as_str())
11589 .map(String::from);
11590
11591 let rrset = ResourceRecordSet {
11592 name: normalized_name.clone(),
11593 record_type: record_type.clone(),
11594 set_identifier: set_identifier.clone(),
11595 weight,
11596 region,
11597 geo_location: None,
11598 failover,
11599 multi_value_answer,
11600 ttl,
11601 resource_records,
11602 alias_target,
11603 health_check_id,
11604 traffic_policy_instance_id: None,
11605 cidr_routing_config: None,
11606 geo_proximity_location: None,
11607 };
11608
11609 let mut accounts = self.route53_state.write();
11610 let state = accounts.entry("000000000000");
11613 let zone = state.hosted_zones.get_mut(&zone_id).ok_or_else(|| {
11614 format!(
11615 "HostedZone {zone_id} not yet provisioned; will retry once it has been provisioned"
11616 )
11617 })?;
11618 zone.resource_record_sets.retain(|r| {
11620 !(r.name == rrset.name
11621 && r.record_type == rrset.record_type
11622 && r.set_identifier == rrset.set_identifier)
11623 });
11624 zone.resource_record_sets.push(rrset);
11625
11626 let physical_id = match &set_identifier {
11627 Some(sid) => format!("{zone_id}|{normalized_name}|{record_type}|{sid}"),
11628 None => format!("{zone_id}|{normalized_name}|{record_type}"),
11629 };
11630 Ok(ProvisionResult::new(physical_id))
11631 }
11632
11633 fn delete_route53_record_set(
11634 &self,
11635 physical_id: &str,
11636 _attributes: &BTreeMap<String, String>,
11637 ) -> Result<(), String> {
11638 let parts: Vec<&str> = physical_id.split('|').collect();
11639 if parts.len() < 3 {
11640 return Ok(());
11641 }
11642 let zone_id = parts[0];
11643 let name = parts[1];
11644 let record_type = parts[2];
11645 let set_identifier = parts.get(3).map(|s| s.to_string());
11646
11647 let mut accounts = self.route53_state.write();
11648 let state = accounts.entry("000000000000");
11651 if let Some(zone) = state.hosted_zones.get_mut(zone_id) {
11652 zone.resource_record_sets.retain(|r| {
11653 !(r.name == name
11654 && r.record_type == record_type
11655 && r.set_identifier == set_identifier)
11656 });
11657 }
11658 Ok(())
11659 }
11660
11661 fn create_route53_health_check(
11662 &self,
11663 resource: &ResourceDefinition,
11664 ) -> Result<ProvisionResult, String> {
11665 let props = &resource.properties;
11666 let cfg_value = props
11667 .get("HealthCheckConfig")
11668 .ok_or("HealthCheckConfig is required")?;
11669
11670 let health_check_type = cfg_value
11671 .get("Type")
11672 .and_then(|v| v.as_str())
11673 .ok_or("HealthCheckConfig.Type is required")?
11674 .to_string();
11675
11676 let cfg = HealthCheckConfig {
11677 ip_address: cfg_value
11678 .get("IPAddress")
11679 .and_then(|v| v.as_str())
11680 .map(String::from),
11681 port: cfg_value
11682 .get("Port")
11683 .and_then(|v| {
11684 v.as_i64()
11685 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11686 })
11687 .map(|n| n as i32),
11688 health_check_type,
11689 resource_path: cfg_value
11690 .get("ResourcePath")
11691 .and_then(|v| v.as_str())
11692 .map(String::from),
11693 fully_qualified_domain_name: cfg_value
11694 .get("FullyQualifiedDomainName")
11695 .and_then(|v| v.as_str())
11696 .map(String::from),
11697 search_string: cfg_value
11698 .get("SearchString")
11699 .and_then(|v| v.as_str())
11700 .map(String::from),
11701 request_interval: cfg_value
11702 .get("RequestInterval")
11703 .and_then(|v| {
11704 v.as_i64()
11705 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11706 })
11707 .map(|n| n as i32),
11708 failure_threshold: cfg_value
11709 .get("FailureThreshold")
11710 .and_then(|v| {
11711 v.as_i64()
11712 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11713 })
11714 .map(|n| n as i32),
11715 measure_latency: cfg_value.get("MeasureLatency").and_then(|v| v.as_bool()),
11716 inverted: cfg_value.get("Inverted").and_then(|v| v.as_bool()),
11717 disabled: cfg_value.get("Disabled").and_then(|v| v.as_bool()),
11718 health_threshold: cfg_value
11719 .get("HealthThreshold")
11720 .and_then(|v| {
11721 v.as_i64()
11722 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11723 })
11724 .map(|n| n as i32),
11725 child_health_checks: cfg_value
11726 .get("ChildHealthChecks")
11727 .and_then(|v| v.as_array())
11728 .map(|arr| fakecloud_route53::model::ChildHealthChecks {
11729 child_health_check: arr
11730 .iter()
11731 .filter_map(|v| v.as_str().map(String::from))
11732 .collect(),
11733 }),
11734 enable_sni: cfg_value.get("EnableSNI").and_then(|v| v.as_bool()),
11735 regions: cfg_value
11736 .get("Regions")
11737 .and_then(|v| v.as_array())
11738 .map(|arr| fakecloud_route53::model::HealthCheckRegions {
11739 region: arr
11740 .iter()
11741 .filter_map(|v| v.as_str().map(String::from))
11742 .collect(),
11743 }),
11744 alarm_identifier: cfg_value.get("AlarmIdentifier").map(|v| {
11745 fakecloud_route53::model::AlarmIdentifier {
11746 region: v
11747 .get("Region")
11748 .and_then(|x| x.as_str())
11749 .unwrap_or("")
11750 .to_string(),
11751 name: v
11752 .get("Name")
11753 .and_then(|x| x.as_str())
11754 .unwrap_or("")
11755 .to_string(),
11756 }
11757 }),
11758 insufficient_data_health_status: cfg_value
11759 .get("InsufficientDataHealthStatus")
11760 .and_then(|v| v.as_str())
11761 .map(String::from),
11762 routing_control_arn: cfg_value
11763 .get("RoutingControlArn")
11764 .and_then(|v| v.as_str())
11765 .map(String::from),
11766 };
11767
11768 let id = Uuid::new_v4().to_string();
11769 let hc = StoredHealthCheck {
11770 id: id.clone(),
11771 caller_reference: format!("cfn-{}", resource.logical_id),
11772 version: 1,
11773 config: cfg,
11774 created_time: Utc::now(),
11775 status: fakecloud_route53::HealthCheckStatus::Success,
11776 last_failure_reason: None,
11777 };
11778
11779 let mut accounts = self.route53_state.write();
11780 let state = accounts.entry("000000000000");
11783 state.health_checks.insert(id.clone(), hc);
11784
11785 Ok(ProvisionResult::new(id.clone()).with("HealthCheckId", id))
11786 }
11787
11788 fn delete_route53_health_check(&self, physical_id: &str) -> Result<(), String> {
11789 let mut accounts = self.route53_state.write();
11790 let state = accounts.entry("000000000000");
11793 state.health_checks.remove(physical_id);
11794 Ok(())
11795 }
11796
11797 fn create_route53_dnssec(
11801 &self,
11802 resource: &ResourceDefinition,
11803 ) -> Result<ProvisionResult, String> {
11804 let zone_id = resource
11805 .properties
11806 .get("HostedZoneId")
11807 .and_then(|v| v.as_str())
11808 .ok_or_else(|| "HostedZoneId is required".to_string())?
11809 .trim_start_matches("/hostedzone/")
11810 .to_string();
11811 let mut accounts = self.route53_state.write();
11812 let state = accounts.entry("000000000000");
11813 if !state.hosted_zones.contains_key(&zone_id) {
11814 return Err(format!("HostedZone {zone_id} does not exist"));
11815 }
11816 state
11817 .dnssec_status
11818 .insert(zone_id.clone(), "SIGNING".to_string());
11819 Ok(ProvisionResult::new(zone_id))
11820 }
11821
11822 fn delete_route53_dnssec(&self, physical_id: &str) -> Result<(), String> {
11823 let mut accounts = self.route53_state.write();
11824 let state = accounts.entry("000000000000");
11825 state.dnssec_status.remove(physical_id);
11826 Ok(())
11827 }
11828
11829 fn create_route53_key_signing_key(
11833 &self,
11834 resource: &ResourceDefinition,
11835 ) -> Result<ProvisionResult, String> {
11836 let props = &resource.properties;
11837 let zone_id = props
11838 .get("HostedZoneId")
11839 .and_then(|v| v.as_str())
11840 .ok_or_else(|| "HostedZoneId is required".to_string())?
11841 .trim_start_matches("/hostedzone/")
11842 .to_string();
11843 let name = props
11844 .get("Name")
11845 .and_then(|v| v.as_str())
11846 .ok_or_else(|| "Name is required".to_string())?
11847 .to_string();
11848 let kms_arn = props
11849 .get("KeyManagementServiceArn")
11850 .and_then(|v| v.as_str())
11851 .ok_or_else(|| "KeyManagementServiceArn is required".to_string())?
11852 .to_string();
11853 let status = props
11854 .get("Status")
11855 .and_then(|v| v.as_str())
11856 .unwrap_or("ACTIVE")
11857 .to_string();
11858 let now = Utc::now();
11859 let key_material = fakecloud_route53::dnssec::derive_keypair(&zone_id, &name);
11865 let key_tag = fakecloud_route53::dnssec::key_tag_for(&key_material.dnskey_public_key);
11866 let mut accounts = self.route53_state.write();
11867 let state = accounts.entry("000000000000");
11868 if !state.hosted_zones.contains_key(&zone_id) {
11869 return Err(format!("HostedZone {zone_id} does not exist"));
11870 }
11871 let zone_name = state
11872 .hosted_zones
11873 .get(&zone_id)
11874 .map(|z| z.name.clone())
11875 .unwrap_or_else(|| ".".to_string());
11876 let ds_digest_hex = fakecloud_route53::dnssec::ds_digest_sha256(
11877 &zone_name,
11878 key_tag,
11879 &key_material.dnskey_public_key,
11880 );
11881 let ksk = fakecloud_route53::StoredKeySigningKey {
11882 hosted_zone_id: zone_id.clone(),
11883 name: name.clone(),
11884 kms_arn,
11885 status,
11886 caller_reference: format!("cfn-{}", Uuid::new_v4()),
11887 created_date: now,
11888 last_modified_date: now,
11889 key_tag: key_tag as i32,
11890 private_key_pem: key_material.private_key_pem,
11891 public_key_der: key_material.public_key_der,
11892 ds_digest_hex,
11893 };
11894 state
11895 .key_signing_keys
11896 .insert((zone_id.clone(), name.clone()), ksk);
11897 Ok(ProvisionResult::new(format!("{zone_id}/{name}")))
11898 }
11899
11900 fn delete_route53_key_signing_key(&self, physical_id: &str) -> Result<(), String> {
11901 let (zone_id, name) = match physical_id.split_once('/') {
11902 Some(parts) => parts,
11903 None => return Ok(()),
11904 };
11905 let mut accounts = self.route53_state.write();
11906 let state = accounts.entry("000000000000");
11907 state
11908 .key_signing_keys
11909 .remove(&(zone_id.to_string(), name.to_string()));
11910 Ok(())
11911 }
11912
11913 fn create_cf_origin_access_identity(
11920 &self,
11921 resource: &ResourceDefinition,
11922 ) -> Result<ProvisionResult, String> {
11923 let props = &resource.properties;
11924 let cfg = props
11925 .get("CloudFrontOriginAccessIdentityConfig")
11926 .ok_or("CloudFrontOriginAccessIdentityConfig is required")?;
11927 let comment = cfg
11928 .get("Comment")
11929 .and_then(|v| v.as_str())
11930 .unwrap_or("")
11931 .to_string();
11932 let caller_reference = format!("cfn-{}", resource.logical_id);
11933
11934 let id = format!(
11935 "E{}",
11936 Uuid::new_v4().simple().to_string()[..13].to_uppercase()
11937 );
11938 let etag = format!(
11939 "E{}",
11940 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
11941 );
11942 let s3_canonical_user_id = format!(
11943 "{:0<64}",
11944 Uuid::new_v4().simple().to_string().to_lowercase()
11945 );
11946
11947 let oai = StoredOriginAccessIdentity {
11948 id: id.clone(),
11949 etag,
11950 s3_canonical_user_id: s3_canonical_user_id.clone(),
11951 config: CloudFrontOriginAccessIdentityConfig {
11952 caller_reference,
11953 comment,
11954 },
11955 };
11956
11957 let mut accounts = self.cloudfront_state.write();
11958 let state = accounts.entry("000000000000");
11959 state.origin_access_identities.insert(id.clone(), oai);
11960
11961 Ok(ProvisionResult::new(id.clone())
11962 .with("Id", id)
11963 .with("S3CanonicalUserId", s3_canonical_user_id))
11964 }
11965
11966 fn delete_cf_origin_access_identity(&self, physical_id: &str) -> Result<(), String> {
11967 let mut accounts = self.cloudfront_state.write();
11968 let state = accounts.entry("000000000000");
11969 state.origin_access_identities.remove(physical_id);
11970 Ok(())
11971 }
11972
11973 fn create_cf_distribution(
11979 &self,
11980 resource: &ResourceDefinition,
11981 ) -> Result<ProvisionResult, String> {
11982 let cfg = resource
11983 .properties
11984 .get("DistributionConfig")
11985 .ok_or_else(|| "DistributionConfig is required".to_string())?;
11986
11987 let origin_entries: Vec<Origin> = cfg
11994 .get("Origins")
11995 .and_then(|v| v.as_array())
11996 .ok_or_else(|| "DistributionConfig.Origins is required".to_string())?
11997 .iter()
11998 .map(|o| {
11999 let mut patched = o.clone();
12000 if let Some(custom) = patched
12001 .get_mut("CustomOriginConfig")
12002 .and_then(|v| v.as_object_mut())
12003 {
12004 if let Some(v) = custom.remove("HTTPPort") {
12005 custom.insert("HttpPort".to_string(), v);
12006 }
12007 if let Some(v) = custom.remove("HTTPSPort") {
12008 custom.insert("HttpsPort".to_string(), v);
12009 }
12010 }
12011 serde_json::from_value::<Origin>(patched)
12012 .map_err(|e| format!("Invalid Origin entry: {e}"))
12013 })
12014 .collect::<Result<Vec<_>, _>>()?;
12015 if origin_entries.is_empty() {
12016 return Err("DistributionConfig.Origins must contain at least one origin".to_string());
12017 }
12018 let origins = Origins {
12019 quantity: origin_entries.len() as i32,
12020 items: Some(OriginItems {
12021 origin: origin_entries,
12022 }),
12023 };
12024
12025 let dcb_value = cfg
12026 .get("DefaultCacheBehavior")
12027 .ok_or_else(|| "DistributionConfig.DefaultCacheBehavior is required".to_string())?;
12028 let default_cache_behavior: DefaultCacheBehavior =
12029 serde_json::from_value(dcb_value.clone())
12030 .map_err(|e| format!("Invalid DefaultCacheBehavior: {e}"))?;
12031
12032 let comment = cfg
12033 .get("Comment")
12034 .and_then(|v| v.as_str())
12035 .unwrap_or("")
12036 .to_string();
12037 let enabled = cfg.get("Enabled").and_then(|v| v.as_bool()).unwrap_or(true);
12038 let price_class = cfg
12039 .get("PriceClass")
12040 .and_then(|v| v.as_str())
12041 .map(|s| s.to_string());
12042 let http_version = cfg
12043 .get("HttpVersion")
12044 .and_then(|v| v.as_str())
12045 .map(|s| s.to_string());
12046 let is_ipv6_enabled = cfg.get("IPV6Enabled").and_then(|v| v.as_bool());
12047 let default_root_object = cfg
12048 .get("DefaultRootObject")
12049 .and_then(|v| v.as_str())
12050 .map(|s| s.to_string());
12051 let web_acl_id = cfg
12052 .get("WebACLId")
12053 .and_then(|v| v.as_str())
12054 .map(|s| s.to_string());
12055
12056 let viewer_certificate: Option<ViewerCertificate> = cfg
12057 .get("ViewerCertificate")
12058 .map(|v| serde_json::from_value(v.clone()))
12059 .transpose()
12060 .map_err(|e| format!("Invalid ViewerCertificate: {e}"))?;
12061
12062 let caller_reference = format!("cfn-{}-{}", resource.logical_id, Uuid::new_v4().simple());
12063
12064 let mut config = DistributionConfig {
12065 caller_reference,
12066 comment,
12067 enabled,
12068 origins,
12069 default_cache_behavior,
12070 ..Default::default()
12071 };
12072 config.price_class = price_class;
12073 config.http_version = http_version;
12074 config.is_ipv6_enabled = is_ipv6_enabled;
12075 config.default_root_object = default_root_object;
12076 config.web_acl_id = web_acl_id;
12077 config.viewer_certificate = viewer_certificate;
12078
12079 let id_suffix: String = Uuid::new_v4()
12082 .simple()
12083 .to_string()
12084 .chars()
12085 .take(13)
12086 .collect::<String>()
12087 .to_uppercase();
12088 let id = format!("E{id_suffix}");
12089 let etag_suffix: String = Uuid::new_v4()
12090 .simple()
12091 .to_string()
12092 .chars()
12093 .take(7)
12094 .collect::<String>()
12095 .to_uppercase();
12096 let etag = format!("E{etag_suffix}");
12097 let domain_name = format!("{}.cloudfront.net", id.to_lowercase());
12098 let arn = format!(
12099 "arn:aws:cloudfront::{}:distribution/{}",
12100 self.account_id, id
12101 );
12102
12103 let stored = StoredDistribution {
12104 id: id.clone(),
12105 arn: arn.clone(),
12106 status: "InProgress".to_string(),
12109 last_modified_time: Utc::now(),
12110 domain_name: domain_name.clone(),
12111 in_progress_invalidation_batches: 0,
12112 etag,
12113 config,
12114 };
12115
12116 let mut accounts = self.cloudfront_state.write();
12117 let state = accounts.entry("000000000000");
12118 state.distributions.insert(id.clone(), stored);
12119 Ok(ProvisionResult::new(id.clone())
12120 .with("Id", id)
12121 .with("DomainName", domain_name)
12122 .with("Arn", arn))
12123 }
12124
12125 fn delete_cf_distribution(&self, physical_id: &str) -> Result<(), String> {
12126 let mut accounts = self.cloudfront_state.write();
12127 let state = accounts.entry("000000000000");
12128 state.distributions.remove(physical_id);
12129 Ok(())
12130 }
12131
12132 fn create_cf_origin_access_control(
12133 &self,
12134 resource: &ResourceDefinition,
12135 ) -> Result<ProvisionResult, String> {
12136 let props = &resource.properties;
12137 let cfg = props
12138 .get("OriginAccessControlConfig")
12139 .ok_or("OriginAccessControlConfig is required")?;
12140 let name = cfg
12141 .get("Name")
12142 .and_then(|v| v.as_str())
12143 .ok_or("OriginAccessControlConfig.Name is required")?
12144 .to_string();
12145 let signing_protocol = cfg
12146 .get("SigningProtocol")
12147 .and_then(|v| v.as_str())
12148 .unwrap_or("sigv4")
12149 .to_string();
12150 let signing_behavior = cfg
12151 .get("SigningBehavior")
12152 .and_then(|v| v.as_str())
12153 .unwrap_or("always")
12154 .to_string();
12155 let origin_type = cfg
12156 .get("OriginAccessControlOriginType")
12157 .and_then(|v| v.as_str())
12158 .ok_or("OriginAccessControlConfig.OriginAccessControlOriginType is required")?
12159 .to_string();
12160 let description = cfg
12161 .get("Description")
12162 .and_then(|v| v.as_str())
12163 .map(String::from);
12164
12165 let id = format!(
12166 "E{}",
12167 Uuid::new_v4().simple().to_string()[..13].to_uppercase()
12168 );
12169 let etag = format!(
12170 "E{}",
12171 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12172 );
12173 let oac = StoredOriginAccessControl {
12174 id: id.clone(),
12175 etag,
12176 config: OriginAccessControlConfig {
12177 name,
12178 description,
12179 signing_protocol,
12180 signing_behavior,
12181 origin_access_control_origin_type: origin_type,
12182 },
12183 };
12184
12185 let mut accounts = self.cloudfront_state.write();
12186 let state = accounts.entry("000000000000");
12187 state.origin_access_controls.insert(id.clone(), oac);
12188
12189 Ok(ProvisionResult::new(id.clone()).with("Id", id))
12190 }
12191
12192 fn delete_cf_origin_access_control(&self, physical_id: &str) -> Result<(), String> {
12193 let mut accounts = self.cloudfront_state.write();
12194 let state = accounts.entry("000000000000");
12195 state.origin_access_controls.remove(physical_id);
12196 Ok(())
12197 }
12198
12199 fn create_cf_public_key(
12200 &self,
12201 resource: &ResourceDefinition,
12202 ) -> Result<ProvisionResult, String> {
12203 let props = &resource.properties;
12204 let cfg = props
12205 .get("PublicKeyConfig")
12206 .ok_or("PublicKeyConfig is required")?;
12207 let name = cfg
12208 .get("Name")
12209 .and_then(|v| v.as_str())
12210 .ok_or("PublicKeyConfig.Name is required")?
12211 .to_string();
12212 let encoded_key = cfg
12213 .get("EncodedKey")
12214 .and_then(|v| v.as_str())
12215 .ok_or("PublicKeyConfig.EncodedKey is required")?
12216 .to_string();
12217 let comment = cfg
12218 .get("Comment")
12219 .and_then(|v| v.as_str())
12220 .map(String::from);
12221 let caller_reference = cfg
12222 .get("CallerReference")
12223 .and_then(|v| v.as_str())
12224 .unwrap_or("")
12225 .to_string();
12226 let caller_reference = if caller_reference.is_empty() {
12227 format!("cfn-{}", resource.logical_id)
12228 } else {
12229 caller_reference
12230 };
12231
12232 let id = format!(
12233 "K{}",
12234 Uuid::new_v4().simple().to_string()[..13].to_uppercase()
12235 );
12236 let etag = format!(
12237 "E{}",
12238 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12239 );
12240
12241 let pk = StoredPublicKey {
12242 id: id.clone(),
12243 etag,
12244 created_time: Utc::now(),
12245 config: PublicKeyConfig {
12246 caller_reference,
12247 name,
12248 encoded_key,
12249 comment,
12250 },
12251 };
12252
12253 let mut accounts = self.cloudfront_state.write();
12254 let state = accounts.entry("000000000000");
12255 state.public_keys.insert(id.clone(), pk);
12256
12257 Ok(ProvisionResult::new(id.clone()).with("Id", id))
12258 }
12259
12260 fn delete_cf_public_key(&self, physical_id: &str) -> Result<(), String> {
12261 let mut accounts = self.cloudfront_state.write();
12262 let state = accounts.entry("000000000000");
12263 state.public_keys.remove(physical_id);
12264 Ok(())
12265 }
12266
12267 fn create_cf_key_group(
12268 &self,
12269 resource: &ResourceDefinition,
12270 ) -> Result<ProvisionResult, String> {
12271 let props = &resource.properties;
12272 let cfg = props
12273 .get("KeyGroupConfig")
12274 .ok_or("KeyGroupConfig is required")?;
12275 let name = cfg
12276 .get("Name")
12277 .and_then(|v| v.as_str())
12278 .ok_or("KeyGroupConfig.Name is required")?
12279 .to_string();
12280 let items: Vec<String> = cfg
12281 .get("Items")
12282 .and_then(|v| v.as_array())
12283 .map(|arr| {
12284 arr.iter()
12285 .filter_map(|v| v.as_str().map(String::from))
12286 .collect()
12287 })
12288 .unwrap_or_default();
12289 let comment = cfg
12290 .get("Comment")
12291 .and_then(|v| v.as_str())
12292 .map(String::from);
12293
12294 let id = format!(
12295 "KG{}",
12296 Uuid::new_v4().simple().to_string()[..12].to_uppercase()
12297 );
12298 let etag = format!(
12299 "E{}",
12300 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12301 );
12302
12303 let kg = StoredKeyGroup {
12304 id: id.clone(),
12305 etag,
12306 last_modified_time: Utc::now(),
12307 config: KeyGroupConfig {
12308 name,
12309 items: KeyGroupItems { public_key: items },
12310 comment,
12311 },
12312 };
12313
12314 let mut accounts = self.cloudfront_state.write();
12315 let state = accounts.entry("000000000000");
12316 state.key_groups.insert(id.clone(), kg);
12317
12318 Ok(ProvisionResult::new(id.clone()).with("Id", id))
12319 }
12320
12321 fn delete_cf_key_group(&self, physical_id: &str) -> Result<(), String> {
12322 let mut accounts = self.cloudfront_state.write();
12323 let state = accounts.entry("000000000000");
12324 state.key_groups.remove(physical_id);
12325 Ok(())
12326 }
12327
12328 fn create_cf_function(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
12329 let props = &resource.properties;
12330 let name = props
12331 .get("Name")
12332 .and_then(|v| v.as_str())
12333 .ok_or("Name is required")?
12334 .to_string();
12335 let function_code = props
12336 .get("FunctionCode")
12337 .and_then(|v| v.as_str())
12338 .ok_or("FunctionCode is required")?
12339 .to_string();
12340 let cfg = props
12341 .get("FunctionConfig")
12342 .ok_or("FunctionConfig is required")?;
12343 let runtime = cfg
12344 .get("Runtime")
12345 .and_then(|v| v.as_str())
12346 .unwrap_or("cloudfront-js-2.0")
12347 .to_string();
12348 let comment = cfg
12349 .get("Comment")
12350 .and_then(|v| v.as_str())
12351 .map(String::from);
12352
12353 let id = format!(
12354 "FN{}",
12355 Uuid::new_v4().simple().to_string()[..12].to_uppercase()
12356 );
12357 let etag = format!(
12358 "E{}",
12359 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12360 );
12361 let function_arn = format!("arn:aws:cloudfront::{}:function/{}", self.account_id, name);
12362
12363 let now = Utc::now();
12364 let func = StoredFunction {
12365 name: name.clone(),
12366 etag,
12367 status: "UNPUBLISHED".to_string(),
12368 stage: "DEVELOPMENT".to_string(),
12369 function_arn: function_arn.clone(),
12370 created_time: now,
12371 last_modified_time: now,
12372 config: FunctionConfig {
12373 comment,
12374 runtime,
12375 key_value_store_associations: None,
12376 },
12377 function_code,
12378 live_function_code: None,
12379 };
12380
12381 let mut accounts = self.cloudfront_state.write();
12382 let state = accounts.entry("000000000000");
12383 state.functions.insert(name.clone(), func);
12386
12387 Ok(ProvisionResult::new(name.clone())
12388 .with("FunctionARN", function_arn)
12389 .with("FunctionMetadata.FunctionARN", id)
12390 .with("Stage", "DEVELOPMENT"))
12391 }
12392
12393 fn delete_cf_function(&self, physical_id: &str) -> Result<(), String> {
12394 let mut accounts = self.cloudfront_state.write();
12395 let state = accounts.entry("000000000000");
12396 state.functions.remove(physical_id);
12397 Ok(())
12398 }
12399
12400 fn create_cf_cache_policy(
12401 &self,
12402 resource: &ResourceDefinition,
12403 ) -> Result<ProvisionResult, String> {
12404 let props = &resource.properties;
12405 let cfg = props
12406 .get("CachePolicyConfig")
12407 .ok_or("CachePolicyConfig is required")?;
12408 let name = cfg
12409 .get("Name")
12410 .and_then(|v| v.as_str())
12411 .ok_or("CachePolicyConfig.Name is required")?
12412 .to_string();
12413 let min_ttl = cfg
12414 .get("MinTTL")
12415 .and_then(|v| {
12416 v.as_i64()
12417 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
12418 })
12419 .unwrap_or(0);
12420 let default_ttl = cfg.get("DefaultTTL").and_then(|v| {
12421 v.as_i64()
12422 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
12423 });
12424 let max_ttl = cfg.get("MaxTTL").and_then(|v| {
12425 v.as_i64()
12426 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
12427 });
12428 let comment = cfg
12429 .get("Comment")
12430 .and_then(|v| v.as_str())
12431 .map(String::from);
12432
12433 let id = format!(
12434 "CP{}",
12435 Uuid::new_v4().simple().to_string()[..12].to_uppercase()
12436 );
12437 let etag = format!(
12438 "E{}",
12439 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12440 );
12441
12442 let cache_policy = StoredCachePolicy {
12443 id: id.clone(),
12444 etag,
12445 last_modified_time: Utc::now(),
12446 config: CachePolicyConfig {
12447 comment,
12448 name,
12449 default_ttl,
12450 max_ttl,
12451 min_ttl,
12452 parameters_in_cache_key_and_forwarded_to_origin: None,
12453 },
12454 policy_type: "custom".to_string(),
12455 };
12456
12457 let mut accounts = self.cloudfront_state.write();
12458 let state = accounts.entry("000000000000");
12459 state.cache_policies.insert(id.clone(), cache_policy);
12460
12461 Ok(ProvisionResult::new(id.clone()).with("Id", id))
12462 }
12463
12464 fn delete_cf_cache_policy(&self, physical_id: &str) -> Result<(), String> {
12465 let mut accounts = self.cloudfront_state.write();
12466 let state = accounts.entry("000000000000");
12467 state.cache_policies.remove(physical_id);
12468 Ok(())
12469 }
12470
12471 fn create_cf_origin_request_policy(
12472 &self,
12473 resource: &ResourceDefinition,
12474 ) -> Result<ProvisionResult, String> {
12475 let props = &resource.properties;
12476 let cfg = props
12477 .get("OriginRequestPolicyConfig")
12478 .ok_or("OriginRequestPolicyConfig is required")?;
12479 let name = cfg
12480 .get("Name")
12481 .and_then(|v| v.as_str())
12482 .ok_or("OriginRequestPolicyConfig.Name is required")?
12483 .to_string();
12484 let header_behavior = cfg
12485 .get("HeadersConfig")
12486 .and_then(|v| v.get("HeaderBehavior"))
12487 .and_then(|v| v.as_str())
12488 .unwrap_or("none")
12489 .to_string();
12490 let cookie_behavior = cfg
12491 .get("CookiesConfig")
12492 .and_then(|v| v.get("CookieBehavior"))
12493 .and_then(|v| v.as_str())
12494 .unwrap_or("none")
12495 .to_string();
12496 let query_string_behavior = cfg
12497 .get("QueryStringsConfig")
12498 .and_then(|v| v.get("QueryStringBehavior"))
12499 .and_then(|v| v.as_str())
12500 .unwrap_or("none")
12501 .to_string();
12502 let comment = cfg
12503 .get("Comment")
12504 .and_then(|v| v.as_str())
12505 .map(String::from);
12506
12507 let id = format!(
12508 "ORP{}",
12509 Uuid::new_v4().simple().to_string()[..11].to_uppercase()
12510 );
12511 let etag = format!(
12512 "E{}",
12513 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12514 );
12515
12516 let policy = StoredOriginRequestPolicy {
12517 id: id.clone(),
12518 etag,
12519 last_modified_time: Utc::now(),
12520 config: OriginRequestPolicyConfig {
12521 comment,
12522 name,
12523 headers_config: OriginRequestPolicyHeadersConfig {
12524 header_behavior,
12525 headers: None,
12526 },
12527 cookies_config: OriginRequestPolicyCookiesConfig {
12528 cookie_behavior,
12529 cookies: None,
12530 },
12531 query_strings_config: OriginRequestPolicyQueryStringsConfig {
12532 query_string_behavior,
12533 query_strings: None,
12534 },
12535 },
12536 policy_type: "custom".to_string(),
12537 };
12538
12539 let mut accounts = self.cloudfront_state.write();
12540 let state = accounts.entry("000000000000");
12541 state.origin_request_policies.insert(id.clone(), policy);
12542
12543 Ok(ProvisionResult::new(id.clone()).with("Id", id))
12544 }
12545
12546 fn delete_cf_origin_request_policy(&self, physical_id: &str) -> Result<(), String> {
12547 let mut accounts = self.cloudfront_state.write();
12548 let state = accounts.entry("000000000000");
12549 state.origin_request_policies.remove(physical_id);
12550 Ok(())
12551 }
12552
12553 fn create_cf_response_headers_policy(
12554 &self,
12555 resource: &ResourceDefinition,
12556 ) -> Result<ProvisionResult, String> {
12557 let props = &resource.properties;
12558 let cfg = props
12559 .get("ResponseHeadersPolicyConfig")
12560 .ok_or("ResponseHeadersPolicyConfig is required")?;
12561 let name = cfg
12562 .get("Name")
12563 .and_then(|v| v.as_str())
12564 .ok_or("ResponseHeadersPolicyConfig.Name is required")?
12565 .to_string();
12566 let comment = cfg
12567 .get("Comment")
12568 .and_then(|v| v.as_str())
12569 .map(String::from);
12570
12571 let id = format!(
12572 "RHP{}",
12573 Uuid::new_v4().simple().to_string()[..11].to_uppercase()
12574 );
12575 let etag = format!(
12576 "E{}",
12577 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12578 );
12579
12580 let policy = StoredResponseHeadersPolicy {
12581 id: id.clone(),
12582 etag,
12583 last_modified_time: Utc::now(),
12584 config: ResponseHeadersPolicyConfig {
12585 comment,
12586 name,
12587 cors_config: None,
12588 security_headers_config: None,
12589 server_timing_headers_config: None,
12590 custom_headers_config: None,
12591 remove_headers_config: None,
12592 },
12593 policy_type: "custom".to_string(),
12594 };
12595
12596 let mut accounts = self.cloudfront_state.write();
12597 let state = accounts.entry("000000000000");
12598 state.response_headers_policies.insert(id.clone(), policy);
12599
12600 Ok(ProvisionResult::new(id.clone()).with("Id", id))
12601 }
12602
12603 fn delete_cf_response_headers_policy(&self, physical_id: &str) -> Result<(), String> {
12604 let mut accounts = self.cloudfront_state.write();
12605 let state = accounts.entry("000000000000");
12606 state.response_headers_policies.remove(physical_id);
12607 Ok(())
12608 }
12609
12610 fn create_sfn_state_machine(
12613 &self,
12614 resource: &ResourceDefinition,
12615 ) -> Result<ProvisionResult, String> {
12616 let props = &resource.properties;
12617 let name = props
12618 .get("StateMachineName")
12619 .and_then(|v| v.as_str())
12620 .map(String::from)
12621 .unwrap_or_else(|| {
12622 let suffix = Uuid::new_v4().simple().to_string();
12623 format!("{}-{}", resource.logical_id, &suffix[..8])
12624 });
12625 let role_arn = props
12626 .get("RoleArn")
12627 .and_then(|v| v.as_str())
12628 .ok_or("RoleArn is required")?
12629 .to_string();
12630 let machine_type_str = props
12631 .get("StateMachineType")
12632 .and_then(|v| v.as_str())
12633 .unwrap_or("STANDARD");
12634 let machine_type = StateMachineType::parse(machine_type_str)
12635 .ok_or_else(|| format!("Invalid StateMachineType: {machine_type_str}"))?;
12636 let definition = props
12637 .get("DefinitionString")
12638 .and_then(|v| v.as_str())
12639 .map(String::from)
12640 .or_else(|| {
12641 props
12642 .get("Definition")
12643 .map(|v| serde_json::to_string(v).unwrap_or_default())
12644 })
12645 .ok_or("Definition or DefinitionString is required")?;
12646 let logging_configuration = props.get("LoggingConfiguration").cloned();
12647 let tracing_configuration = props.get("TracingConfiguration").cloned();
12648
12649 let arn = format!(
12650 "arn:aws:states:{}:{}:stateMachine:{}",
12651 self.region, self.account_id, name
12652 );
12653 let now = Utc::now();
12654 let revision_id = Uuid::new_v4().to_string();
12655
12656 let sm = StateMachine {
12657 name: name.clone(),
12658 arn: arn.clone(),
12659 definition,
12660 role_arn,
12661 machine_type,
12662 status: StateMachineStatus::Active,
12663 creation_date: now,
12664 update_date: now,
12665 tags: BTreeMap::new(),
12666 revision_id,
12667 logging_configuration,
12668 tracing_configuration,
12669 description: String::new(),
12670 };
12671
12672 let mut accounts = self.stepfunctions_state.write();
12673 let state = accounts.get_or_create(&self.account_id);
12674 state.state_machines.insert(arn.clone(), sm);
12675
12676 Ok(ProvisionResult::new(arn.clone())
12677 .with("Arn", arn.clone())
12678 .with("Name", name)
12679 .with("StateMachineRevisionId", "INITIAL"))
12680 }
12681
12682 fn delete_sfn_state_machine(&self, physical_id: &str) -> Result<(), String> {
12683 let mut accounts = self.stepfunctions_state.write();
12684 let state = accounts.get_or_create(&self.account_id);
12685 state.state_machines.remove(physical_id);
12686 Ok(())
12687 }
12688
12689 fn create_sfn_activity(
12690 &self,
12691 resource: &ResourceDefinition,
12692 ) -> Result<ProvisionResult, String> {
12693 let props = &resource.properties;
12694 let name = props
12695 .get("Name")
12696 .and_then(|v| v.as_str())
12697 .ok_or("Name is required")?
12698 .to_string();
12699 let arn = format!(
12700 "arn:aws:states:{}:{}:activity:{}",
12701 self.region, self.account_id, name
12702 );
12703 let activity = SfnActivity {
12704 name: name.clone(),
12705 arn: arn.clone(),
12706 creation_date: Utc::now(),
12707 tags: BTreeMap::new(),
12708 };
12709
12710 let mut accounts = self.stepfunctions_state.write();
12711 let state = accounts.get_or_create(&self.account_id);
12712 state.activities.insert(arn.clone(), activity);
12713
12714 Ok(ProvisionResult::new(arn.clone())
12715 .with("Arn", arn)
12716 .with("Name", name))
12717 }
12718
12719 fn delete_sfn_activity(&self, physical_id: &str) -> Result<(), String> {
12720 let mut accounts = self.stepfunctions_state.write();
12721 let state = accounts.get_or_create(&self.account_id);
12722 state.activities.remove(physical_id);
12723 Ok(())
12724 }
12725
12726 fn create_sfn_version(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
12727 let props = &resource.properties;
12728 let sm_arn = props
12729 .get("StateMachineArn")
12730 .and_then(|v| v.as_str())
12731 .ok_or("StateMachineArn is required")?
12732 .to_string();
12733 let description = props
12734 .get("Description")
12735 .and_then(|v| v.as_str())
12736 .unwrap_or("")
12737 .to_string();
12738 let revision_id = props
12739 .get("StateMachineRevisionId")
12740 .and_then(|v| v.as_str())
12741 .unwrap_or("INITIAL")
12742 .to_string();
12743
12744 let mut accounts = self.stepfunctions_state.write();
12745 let state = accounts.get_or_create(&self.account_id);
12746
12747 let next_version = state
12749 .state_machine_versions
12750 .values()
12751 .filter(|v| v.state_machine_arn == sm_arn)
12752 .map(|v| v.version)
12753 .max()
12754 .unwrap_or(0)
12755 + 1;
12756 let version_arn = format!("{sm_arn}:{next_version}");
12757
12758 let version = StateMachineVersion {
12759 state_machine_arn: sm_arn,
12760 version: next_version,
12761 revision_id,
12762 description,
12763 creation_date: Utc::now(),
12764 };
12765 state
12766 .state_machine_versions
12767 .insert(version_arn.clone(), version);
12768
12769 Ok(ProvisionResult::new(version_arn.clone()).with("Arn", version_arn))
12770 }
12771
12772 fn delete_sfn_version(&self, physical_id: &str) -> Result<(), String> {
12773 let mut accounts = self.stepfunctions_state.write();
12774 let state = accounts.get_or_create(&self.account_id);
12775 state.state_machine_versions.remove(physical_id);
12776 Ok(())
12777 }
12778
12779 fn create_sfn_alias(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
12780 let props = &resource.properties;
12781 let name = props
12782 .get("Name")
12783 .and_then(|v| v.as_str())
12784 .ok_or("Name is required")?
12785 .to_string();
12786 let description = props
12787 .get("Description")
12788 .and_then(|v| v.as_str())
12789 .unwrap_or("")
12790 .to_string();
12791 let routes_value = props
12792 .get("RoutingConfiguration")
12793 .and_then(|v| v.as_array())
12794 .ok_or("RoutingConfiguration is required")?;
12795 let routing_configuration: Vec<AliasRoute> = routes_value
12796 .iter()
12797 .map(|r| AliasRoute {
12798 state_machine_version_arn: r
12799 .get("StateMachineVersionArn")
12800 .and_then(|x| x.as_str())
12801 .unwrap_or("")
12802 .to_string(),
12803 weight: r
12804 .get("Weight")
12805 .and_then(|x| {
12806 x.as_i64()
12807 .or_else(|| x.as_str().and_then(|s| s.parse::<i64>().ok()))
12808 })
12809 .map(|w| w as i32)
12810 .unwrap_or(0),
12811 })
12812 .collect();
12813
12814 let first_version_arn = routing_configuration
12815 .first()
12816 .map(|r| r.state_machine_version_arn.clone())
12817 .unwrap_or_default();
12818 let sm_arn_root = first_version_arn
12821 .rsplit_once(':')
12822 .map(|(root, _)| root.to_string())
12823 .unwrap_or_else(|| {
12824 format!(
12825 "arn:aws:states:{}:{}:stateMachine:unknown",
12826 self.region, self.account_id
12827 )
12828 });
12829 let arn = format!("{sm_arn_root}:{name}");
12830 let now = Utc::now();
12831 let alias = StateMachineAlias {
12832 name: name.clone(),
12833 arn: arn.clone(),
12834 description,
12835 routing_configuration,
12836 creation_date: now,
12837 update_date: now,
12838 };
12839
12840 let mut accounts = self.stepfunctions_state.write();
12841 let state = accounts.get_or_create(&self.account_id);
12842 state.state_machine_aliases.insert(arn.clone(), alias);
12843
12844 Ok(ProvisionResult::new(arn.clone())
12845 .with("Arn", arn)
12846 .with("Name", name))
12847 }
12848
12849 fn delete_sfn_alias(&self, physical_id: &str) -> Result<(), String> {
12850 let mut accounts = self.stepfunctions_state.write();
12851 let state = accounts.get_or_create(&self.account_id);
12852 state.state_machine_aliases.remove(physical_id);
12853 Ok(())
12854 }
12855
12856 fn create_wafv2_web_acl(
12864 &self,
12865 resource: &ResourceDefinition,
12866 ) -> Result<ProvisionResult, String> {
12867 let props = &resource.properties;
12868 let name = props
12869 .get("Name")
12870 .and_then(|v| v.as_str())
12871 .ok_or("Name is required")?
12872 .to_string();
12873 let scope = props
12874 .get("Scope")
12875 .and_then(|v| v.as_str())
12876 .ok_or("Scope is required")?
12877 .to_string();
12878 let default_action = props
12879 .get("DefaultAction")
12880 .cloned()
12881 .unwrap_or_else(|| serde_json::json!({"Allow": {}}));
12882 let description = props
12883 .get("Description")
12884 .and_then(|v| v.as_str())
12885 .map(String::from);
12886 let rules = props
12887 .get("Rules")
12888 .and_then(|v| v.as_array())
12889 .cloned()
12890 .unwrap_or_default();
12891 let visibility_config = props
12892 .get("VisibilityConfig")
12893 .cloned()
12894 .unwrap_or_else(|| serde_json::json!({}));
12895 let capacity = props.get("Capacity").and_then(|v| v.as_i64()).unwrap_or(0);
12896
12897 let id = Uuid::new_v4().to_string();
12898 let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
12899 ("us-east-1", "global".to_string())
12900 } else {
12901 (self.region.as_str(), self.region.clone())
12902 };
12903 let arn = format!(
12904 "arn:aws:wafv2:{}:{}:{}/webacl/{}/{}",
12905 region_in_arn, self.account_id, scope_seg, name, id
12906 );
12907 let acl = WebAcl {
12908 id: id.clone(),
12909 name: name.clone(),
12910 arn: arn.clone(),
12911 scope: scope.clone(),
12912 default_action,
12913 description,
12914 rules,
12915 visibility_config,
12916 capacity,
12917 lock_token: Uuid::new_v4().simple().to_string(),
12918 label_namespace: format!("awswaf:{}:webacl:{}:", self.account_id, name),
12919 custom_response_bodies: BTreeMap::new(),
12920 captcha_config: None,
12921 challenge_config: None,
12922 token_domains: Vec::new(),
12923 association_config: None,
12924 data_protection_config: None,
12925 on_source_d_do_s_protection_config: None,
12926 application_config: None,
12927 retrofitted_by_firewall_manager: false,
12928 pre_process_firewall_manager_rule_groups: Vec::new(),
12929 post_process_firewall_manager_rule_groups: Vec::new(),
12930 managed_by_firewall_manager: false,
12931 created_time: Utc::now(),
12932 };
12933
12934 let mut accounts = self.wafv2_state.write();
12935 let state = accounts
12936 .accounts
12937 .entry(self.account_id.clone())
12938 .or_default();
12939 state.web_acls.insert((scope.clone(), name.clone()), acl);
12940
12941 Ok(ProvisionResult::new(arn.clone())
12942 .with("Arn", arn)
12943 .with("Id", id)
12944 .with("Name", name)
12945 .with("Capacity", capacity.to_string()))
12946 }
12947
12948 fn delete_wafv2_web_acl(&self, physical_id: &str) -> Result<(), String> {
12949 let mut accounts = self.wafv2_state.write();
12950 let state = accounts
12951 .accounts
12952 .entry(self.account_id.clone())
12953 .or_default();
12954 state.web_acls.retain(|_, v| v.arn != physical_id);
12955 Ok(())
12956 }
12957
12958 fn create_wafv2_ip_set(
12959 &self,
12960 resource: &ResourceDefinition,
12961 ) -> Result<ProvisionResult, String> {
12962 let props = &resource.properties;
12963 let name = props
12964 .get("Name")
12965 .and_then(|v| v.as_str())
12966 .ok_or("Name is required")?
12967 .to_string();
12968 let scope = props
12969 .get("Scope")
12970 .and_then(|v| v.as_str())
12971 .ok_or("Scope is required")?
12972 .to_string();
12973 let ip_address_version = props
12974 .get("IPAddressVersion")
12975 .and_then(|v| v.as_str())
12976 .ok_or("IPAddressVersion is required")?
12977 .to_string();
12978 let addresses: Vec<String> = props
12979 .get("Addresses")
12980 .and_then(|v| v.as_array())
12981 .map(|arr| {
12982 arr.iter()
12983 .filter_map(|v| v.as_str().map(String::from))
12984 .collect()
12985 })
12986 .unwrap_or_default();
12987 let description = props
12988 .get("Description")
12989 .and_then(|v| v.as_str())
12990 .map(String::from);
12991
12992 let id = Uuid::new_v4().to_string();
12993 let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
12994 ("us-east-1", "global".to_string())
12995 } else {
12996 (self.region.as_str(), self.region.clone())
12997 };
12998 let arn = format!(
12999 "arn:aws:wafv2:{}:{}:{}/ipset/{}/{}",
13000 region_in_arn, self.account_id, scope_seg, name, id
13001 );
13002 let ip_set = IpSet {
13003 id: id.clone(),
13004 name: name.clone(),
13005 arn: arn.clone(),
13006 scope: scope.clone(),
13007 description,
13008 ip_address_version,
13009 addresses,
13010 lock_token: Uuid::new_v4().simple().to_string(),
13011 created_time: Utc::now(),
13012 };
13013
13014 let mut accounts = self.wafv2_state.write();
13015 let state = accounts
13016 .accounts
13017 .entry(self.account_id.clone())
13018 .or_default();
13019 state.ip_sets.insert((scope, name.clone()), ip_set);
13020
13021 Ok(ProvisionResult::new(arn.clone())
13022 .with("Arn", arn)
13023 .with("Id", id)
13024 .with("Name", name))
13025 }
13026
13027 fn delete_wafv2_ip_set(&self, physical_id: &str) -> Result<(), String> {
13028 let mut accounts = self.wafv2_state.write();
13029 let state = accounts
13030 .accounts
13031 .entry(self.account_id.clone())
13032 .or_default();
13033 state.ip_sets.retain(|_, v| v.arn != physical_id);
13034 Ok(())
13035 }
13036
13037 fn create_wafv2_regex_pattern_set(
13038 &self,
13039 resource: &ResourceDefinition,
13040 ) -> Result<ProvisionResult, String> {
13041 let props = &resource.properties;
13042 let name = props
13043 .get("Name")
13044 .and_then(|v| v.as_str())
13045 .ok_or("Name is required")?
13046 .to_string();
13047 let scope = props
13048 .get("Scope")
13049 .and_then(|v| v.as_str())
13050 .ok_or("Scope is required")?
13051 .to_string();
13052 let regular_expressions: Vec<serde_json::Value> = props
13053 .get("RegularExpressionList")
13054 .and_then(|v| v.as_array())
13055 .map(|arr| {
13056 arr.iter()
13057 .map(|s| {
13058 if let Some(s) = s.as_str() {
13059 serde_json::json!({"RegexString": s})
13060 } else {
13061 s.clone()
13062 }
13063 })
13064 .collect()
13065 })
13066 .unwrap_or_default();
13067 let description = props
13068 .get("Description")
13069 .and_then(|v| v.as_str())
13070 .map(String::from);
13071
13072 let id = Uuid::new_v4().to_string();
13073 let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
13074 ("us-east-1", "global".to_string())
13075 } else {
13076 (self.region.as_str(), self.region.clone())
13077 };
13078 let arn = format!(
13079 "arn:aws:wafv2:{}:{}:{}/regexpatternset/{}/{}",
13080 region_in_arn, self.account_id, scope_seg, name, id
13081 );
13082 let set = RegexPatternSet {
13083 id: id.clone(),
13084 name: name.clone(),
13085 arn: arn.clone(),
13086 scope: scope.clone(),
13087 description,
13088 regular_expressions,
13089 lock_token: Uuid::new_v4().simple().to_string(),
13090 created_time: Utc::now(),
13091 };
13092
13093 let mut accounts = self.wafv2_state.write();
13094 let state = accounts
13095 .accounts
13096 .entry(self.account_id.clone())
13097 .or_default();
13098 state.regex_pattern_sets.insert((scope, name.clone()), set);
13099
13100 Ok(ProvisionResult::new(arn.clone())
13101 .with("Arn", arn)
13102 .with("Id", id)
13103 .with("Name", name))
13104 }
13105
13106 fn delete_wafv2_regex_pattern_set(&self, physical_id: &str) -> Result<(), String> {
13107 let mut accounts = self.wafv2_state.write();
13108 let state = accounts
13109 .accounts
13110 .entry(self.account_id.clone())
13111 .or_default();
13112 state.regex_pattern_sets.retain(|_, v| v.arn != physical_id);
13113 Ok(())
13114 }
13115
13116 fn create_wafv2_rule_group(
13117 &self,
13118 resource: &ResourceDefinition,
13119 ) -> Result<ProvisionResult, String> {
13120 let props = &resource.properties;
13121 let name = props
13122 .get("Name")
13123 .and_then(|v| v.as_str())
13124 .ok_or("Name is required")?
13125 .to_string();
13126 let scope = props
13127 .get("Scope")
13128 .and_then(|v| v.as_str())
13129 .ok_or("Scope is required")?
13130 .to_string();
13131 let capacity = props
13132 .get("Capacity")
13133 .and_then(|v| v.as_i64())
13134 .ok_or("Capacity is required")?;
13135 let description = props
13136 .get("Description")
13137 .and_then(|v| v.as_str())
13138 .map(String::from);
13139 let rules = props
13140 .get("Rules")
13141 .and_then(|v| v.as_array())
13142 .cloned()
13143 .unwrap_or_default();
13144 let visibility_config = props
13145 .get("VisibilityConfig")
13146 .cloned()
13147 .unwrap_or_else(|| serde_json::json!({}));
13148
13149 let id = Uuid::new_v4().to_string();
13150 let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
13151 ("us-east-1", "global".to_string())
13152 } else {
13153 (self.region.as_str(), self.region.clone())
13154 };
13155 let arn = format!(
13156 "arn:aws:wafv2:{}:{}:{}/rulegroup/{}/{}",
13157 region_in_arn, self.account_id, scope_seg, name, id
13158 );
13159 let rg = RuleGroup {
13160 id: id.clone(),
13161 name: name.clone(),
13162 arn: arn.clone(),
13163 scope: scope.clone(),
13164 capacity,
13165 description,
13166 rules,
13167 visibility_config,
13168 lock_token: Uuid::new_v4().simple().to_string(),
13169 label_namespace: format!("awswaf:{}:rulegroup:{}:", self.account_id, name),
13170 custom_response_bodies: BTreeMap::new(),
13171 available_labels: Vec::new(),
13172 consumed_labels: Vec::new(),
13173 created_time: Utc::now(),
13174 };
13175
13176 let mut accounts = self.wafv2_state.write();
13177 let state = accounts
13178 .accounts
13179 .entry(self.account_id.clone())
13180 .or_default();
13181 state.rule_groups.insert((scope, name.clone()), rg);
13182
13183 Ok(ProvisionResult::new(arn.clone())
13184 .with("Arn", arn)
13185 .with("Id", id)
13186 .with("Name", name)
13187 .with("Capacity", capacity.to_string()))
13188 }
13189
13190 fn delete_wafv2_rule_group(&self, physical_id: &str) -> Result<(), String> {
13191 let mut accounts = self.wafv2_state.write();
13192 let state = accounts
13193 .accounts
13194 .entry(self.account_id.clone())
13195 .or_default();
13196 state.rule_groups.retain(|_, v| v.arn != physical_id);
13197 Ok(())
13198 }
13199
13200 fn create_wafv2_logging_configuration(
13201 &self,
13202 resource: &ResourceDefinition,
13203 ) -> Result<ProvisionResult, String> {
13204 let props = &resource.properties;
13205 let resource_arn = props
13206 .get("ResourceArn")
13207 .and_then(|v| v.as_str())
13208 .ok_or("ResourceArn is required")?
13209 .to_string();
13210 let cfg = serde_json::json!({
13211 "ResourceArn": resource_arn,
13212 "LogDestinationConfigs": props.get("LogDestinationConfigs").cloned().unwrap_or_else(|| serde_json::json!([])),
13213 "RedactedFields": props.get("RedactedFields").cloned().unwrap_or_else(|| serde_json::json!([])),
13214 "LoggingFilter": props.get("LoggingFilter").cloned(),
13215 });
13216
13217 let mut accounts = self.wafv2_state.write();
13218 let state = accounts
13219 .accounts
13220 .entry(self.account_id.clone())
13221 .or_default();
13222 state.logging_configs.insert(resource_arn.clone(), cfg);
13223
13224 Ok(ProvisionResult::new(resource_arn))
13225 }
13226
13227 fn delete_wafv2_logging_configuration(&self, physical_id: &str) -> Result<(), String> {
13228 let mut accounts = self.wafv2_state.write();
13229 let state = accounts
13230 .accounts
13231 .entry(self.account_id.clone())
13232 .or_default();
13233 state.logging_configs.remove(physical_id);
13234 Ok(())
13235 }
13236
13237 fn create_wafv2_web_acl_association(
13238 &self,
13239 resource: &ResourceDefinition,
13240 ) -> Result<ProvisionResult, String> {
13241 let props = &resource.properties;
13242 let resource_arn = props
13243 .get("ResourceArn")
13244 .and_then(|v| v.as_str())
13245 .ok_or("ResourceArn is required")?
13246 .to_string();
13247 let web_acl_arn = props
13248 .get("WebACLArn")
13249 .and_then(|v| v.as_str())
13250 .ok_or("WebACLArn is required")?
13251 .to_string();
13252
13253 let mut accounts = self.wafv2_state.write();
13254 let state = accounts
13255 .accounts
13256 .entry(self.account_id.clone())
13257 .or_default();
13258 state.associations.insert(resource_arn.clone(), web_acl_arn);
13259
13260 Ok(ProvisionResult::new(resource_arn))
13262 }
13263
13264 fn delete_wafv2_web_acl_association(&self, physical_id: &str) -> Result<(), String> {
13265 let mut accounts = self.wafv2_state.write();
13266 let state = accounts
13267 .accounts
13268 .entry(self.account_id.clone())
13269 .or_default();
13270 state.associations.remove(physical_id);
13271 Ok(())
13272 }
13273
13274 fn create_apigw_rest_api(
13277 &self,
13278 resource: &ResourceDefinition,
13279 ) -> Result<ProvisionResult, String> {
13280 let props = &resource.properties;
13281 let name = props
13282 .get("Name")
13283 .and_then(|v| v.as_str())
13284 .ok_or("Name is required")?
13285 .to_string();
13286 let description = props
13287 .get("Description")
13288 .and_then(|v| v.as_str())
13289 .map(String::from);
13290 let api_key_source = props
13291 .get("ApiKeySourceType")
13292 .and_then(|v| v.as_str())
13293 .unwrap_or("HEADER")
13294 .to_string();
13295 let endpoint_configuration = props
13296 .get("EndpointConfiguration")
13297 .cloned()
13298 .unwrap_or_else(|| serde_json::json!({"types": ["EDGE"]}));
13299 let policy = props
13300 .get("Policy")
13301 .map(|v| v.to_string().trim_matches('"').to_string());
13302 let binary_media_types: Vec<String> = props
13303 .get("BinaryMediaTypes")
13304 .and_then(|v| v.as_array())
13305 .map(|arr| {
13306 arr.iter()
13307 .filter_map(|v| v.as_str().map(String::from))
13308 .collect()
13309 })
13310 .unwrap_or_default();
13311 let minimum_compression_size = props.get("MinimumCompressionSize").and_then(|v| v.as_i64());
13312 let disable_execute_api_endpoint = props
13313 .get("DisableExecuteApiEndpoint")
13314 .and_then(|v| v.as_bool())
13315 .unwrap_or(false);
13316 let import_source = if props.get("Body").is_some() {
13320 Some("Body".to_string())
13321 } else if props.get("BodyS3Location").is_some() {
13322 Some("BodyS3Location".to_string())
13323 } else if props.get("CloneFrom").is_some() {
13324 Some("CloneFrom".to_string())
13325 } else {
13326 None
13327 };
13328 let tags = parse_acm_tags(props.get("Tags"));
13329
13330 let id = apigw_make_id();
13331 let root_resource_id = apigw_make_id();
13332 let now = Utc::now();
13333
13334 let api = ApiGwRestApi {
13335 id: id.clone(),
13336 name,
13337 description,
13338 version: props
13339 .get("Version")
13340 .and_then(|v| v.as_str())
13341 .map(String::from),
13342 created_date: now,
13343 api_key_source,
13344 endpoint_configuration,
13345 policy,
13346 binary_media_types,
13347 minimum_compression_size,
13348 disable_execute_api_endpoint,
13349 root_resource_id: root_resource_id.clone(),
13350 tags,
13351 import_source,
13352 };
13353
13354 let mut accounts = self.apigateway_state.write();
13355 let state = accounts.get_or_create(&self.account_id);
13356 state.apis.insert(id.clone(), api);
13357 let mut resources = BTreeMap::new();
13358 resources.insert(
13359 root_resource_id.clone(),
13360 ApiGwResource {
13361 id: root_resource_id.clone(),
13362 parent_id: None,
13363 path_part: None,
13364 path: "/".to_string(),
13365 },
13366 );
13367 state.resources.insert(id.clone(), resources);
13368
13369 Ok(ProvisionResult::new(id.clone())
13370 .with("RestApiId", id.clone())
13371 .with("RootResourceId", root_resource_id))
13372 }
13373
13374 fn update_apigw_rest_api(
13375 &self,
13376 existing: &StackResource,
13377 resource: &ResourceDefinition,
13378 ) -> Result<ProvisionResult, String> {
13379 let props = &resource.properties;
13380 let id = existing.physical_id.clone();
13381 let mut accounts = self.apigateway_state.write();
13382 let state = accounts.get_or_create(&self.account_id);
13383 let api = state
13384 .apis
13385 .get_mut(&id)
13386 .ok_or_else(|| format!("RestApi {id} not found for update"))?;
13387 if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
13388 api.name = name.to_string();
13389 }
13390 if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
13391 api.description = Some(desc.to_string());
13392 }
13393 if let Some(source) = props.get("ApiKeySourceType").and_then(|v| v.as_str()) {
13394 api.api_key_source = source.to_string();
13395 }
13396 if let Some(ep) = props.get("EndpointConfiguration").cloned() {
13397 api.endpoint_configuration = ep;
13398 }
13399 if let Some(arr) = props.get("BinaryMediaTypes").and_then(|v| v.as_array()) {
13400 api.binary_media_types = arr
13401 .iter()
13402 .filter_map(|v| v.as_str().map(String::from))
13403 .collect();
13404 }
13405 if let Some(size) = props.get("MinimumCompressionSize").and_then(|v| v.as_i64()) {
13406 api.minimum_compression_size = Some(size);
13407 }
13408 if let Some(b) = props
13409 .get("DisableExecuteApiEndpoint")
13410 .and_then(|v| v.as_bool())
13411 {
13412 api.disable_execute_api_endpoint = b;
13413 }
13414 if props.get("Tags").is_some() {
13415 api.tags = parse_acm_tags(props.get("Tags"));
13416 }
13417 let root = api.root_resource_id.clone();
13418 Ok(ProvisionResult::new(id.clone())
13419 .with("RestApiId", id)
13420 .with("RootResourceId", root))
13421 }
13422
13423 fn delete_apigw_rest_api(&self, physical_id: &str) -> Result<(), String> {
13424 let mut accounts = self.apigateway_state.write();
13425 let state = accounts.get_or_create(&self.account_id);
13426 state.apis.remove(physical_id);
13427 state.resources.remove(physical_id);
13428 let prefix = format!("{physical_id}/");
13429 state.methods.retain(|k, _| !k.starts_with(&prefix));
13430 state.integrations.retain(|k, _| !k.starts_with(&prefix));
13431 state
13432 .integration_responses
13433 .retain(|k, _| !k.starts_with(&prefix));
13434 state
13435 .method_responses
13436 .retain(|k, _| !k.starts_with(&prefix));
13437 state.deployments.remove(physical_id);
13438 state.stages.remove(physical_id);
13439 state.models.remove(physical_id);
13440 state.request_validators.remove(physical_id);
13441 state.authorizers.remove(physical_id);
13442 state.gateway_responses.remove(physical_id);
13443 Ok(())
13444 }
13445
13446 fn create_apigw_resource(
13447 &self,
13448 resource: &ResourceDefinition,
13449 ) -> Result<ProvisionResult, String> {
13450 let props = &resource.properties;
13451 let rest_api_id = props
13452 .get("RestApiId")
13453 .and_then(|v| v.as_str())
13454 .ok_or("RestApiId is required")?
13455 .to_string();
13456 let parent_id = props
13457 .get("ParentId")
13458 .and_then(|v| v.as_str())
13459 .ok_or("ParentId is required")?
13460 .to_string();
13461 let path_part = props
13462 .get("PathPart")
13463 .and_then(|v| v.as_str())
13464 .ok_or("PathPart is required")?
13465 .to_string();
13466
13467 let mut accounts = self.apigateway_state.write();
13468 let state = accounts.get_or_create(&self.account_id);
13469 let api_resources = state
13470 .resources
13471 .get(&rest_api_id)
13472 .ok_or_else(|| format!("RestApi {rest_api_id} not found"))?;
13473 let parent = api_resources
13474 .get(&parent_id)
13475 .ok_or_else(|| format!("Parent resource {parent_id} not found"))?;
13476 let parent_path = parent.path.clone();
13477 let path = if parent_path == "/" {
13478 format!("/{path_part}")
13479 } else {
13480 format!("{parent_path}/{path_part}")
13481 };
13482
13483 let id = apigw_make_id();
13484 let new_resource = ApiGwResource {
13485 id: id.clone(),
13486 parent_id: Some(parent_id),
13487 path_part: Some(path_part),
13488 path,
13489 };
13490 state
13491 .resources
13492 .entry(rest_api_id.clone())
13493 .or_default()
13494 .insert(id.clone(), new_resource);
13495
13496 Ok(ProvisionResult::new(id.clone())
13497 .with("ResourceId", id)
13498 .with("RestApiId", rest_api_id))
13499 }
13500
13501 fn delete_apigw_resource(
13502 &self,
13503 physical_id: &str,
13504 attributes: &BTreeMap<String, String>,
13505 ) -> Result<(), String> {
13506 let Some(rest_api_id) = attributes.get("RestApiId") else {
13507 return Ok(());
13508 };
13509 let mut accounts = self.apigateway_state.write();
13510 let state = accounts.get_or_create(&self.account_id);
13511 if let Some(map) = state.resources.get_mut(rest_api_id) {
13512 map.remove(physical_id);
13513 }
13514 let prefix = format!("{rest_api_id}/{physical_id}/");
13515 state.methods.retain(|k, _| !k.starts_with(&prefix));
13516 state.integrations.retain(|k, _| !k.starts_with(&prefix));
13517 Ok(())
13518 }
13519
13520 fn create_apigw_method(
13521 &self,
13522 resource: &ResourceDefinition,
13523 ) -> Result<ProvisionResult, String> {
13524 let props = &resource.properties;
13525 let rest_api_id = props
13526 .get("RestApiId")
13527 .and_then(|v| v.as_str())
13528 .ok_or("RestApiId is required")?
13529 .to_string();
13530 let resource_id = props
13531 .get("ResourceId")
13532 .and_then(|v| v.as_str())
13533 .ok_or("ResourceId is required")?
13534 .to_string();
13535 let http_method = props
13536 .get("HttpMethod")
13537 .and_then(|v| v.as_str())
13538 .ok_or("HttpMethod is required")?
13539 .to_uppercase();
13540 let authorization_type = props
13541 .get("AuthorizationType")
13542 .and_then(|v| v.as_str())
13543 .unwrap_or("NONE")
13544 .to_string();
13545 let authorizer_id = props
13546 .get("AuthorizerId")
13547 .and_then(|v| v.as_str())
13548 .map(String::from);
13549 let api_key_required = props
13550 .get("ApiKeyRequired")
13551 .and_then(|v| v.as_bool())
13552 .unwrap_or(false);
13553 let operation_name = props
13554 .get("OperationName")
13555 .and_then(|v| v.as_str())
13556 .map(String::from);
13557 let request_validator_id = props
13558 .get("RequestValidatorId")
13559 .and_then(|v| v.as_str())
13560 .map(String::from);
13561 let request_parameters: BTreeMap<String, bool> = props
13562 .get("RequestParameters")
13563 .and_then(|v| v.as_object())
13564 .map(|obj| {
13565 obj.iter()
13566 .map(|(k, v)| (k.clone(), v.as_bool().unwrap_or(false)))
13567 .collect()
13568 })
13569 .unwrap_or_default();
13570 let request_models: BTreeMap<String, String> = props
13571 .get("RequestModels")
13572 .and_then(|v| v.as_object())
13573 .map(|obj| {
13574 obj.iter()
13575 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
13576 .collect()
13577 })
13578 .unwrap_or_default();
13579 let authorization_scopes: Vec<String> = props
13580 .get("AuthorizationScopes")
13581 .and_then(|v| v.as_array())
13582 .map(|arr| {
13583 arr.iter()
13584 .filter_map(|v| v.as_str().map(String::from))
13585 .collect()
13586 })
13587 .unwrap_or_default();
13588
13589 let composite_key = format!("{rest_api_id}/{resource_id}/{http_method}");
13590 let method = ApiGwMethod {
13591 rest_api_id: rest_api_id.clone(),
13592 resource_id: resource_id.clone(),
13593 http_method: http_method.clone(),
13594 authorization_type,
13595 authorizer_id,
13596 api_key_required,
13597 operation_name,
13598 request_parameters,
13599 request_models,
13600 request_validator_id,
13601 authorization_scopes,
13602 };
13603
13604 let mut accounts = self.apigateway_state.write();
13605 let state = accounts.get_or_create(&self.account_id);
13606 if !state.apis.contains_key(&rest_api_id) {
13607 return Err(format!("RestApi {rest_api_id} not found"));
13608 }
13609 let resource_known = state
13613 .resources
13614 .get(&rest_api_id)
13615 .map(|m| m.contains_key(&resource_id))
13616 .unwrap_or(false);
13617 if !resource_known {
13618 return Err(format!(
13619 "Resource {resource_id} not yet provisioned for api {rest_api_id}"
13620 ));
13621 }
13622 state.methods.insert(composite_key.clone(), method);
13623
13624 if let Some(integ_props) = props.get("Integration").and_then(|v| v.as_object()) {
13625 let integration = ApiGwIntegration {
13626 rest_api_id: rest_api_id.clone(),
13627 resource_id: resource_id.clone(),
13628 http_method: http_method.clone(),
13629 integration_type: integ_props
13630 .get("Type")
13631 .and_then(|v| v.as_str())
13632 .unwrap_or("MOCK")
13633 .to_string(),
13634 integration_http_method: integ_props
13635 .get("IntegrationHttpMethod")
13636 .and_then(|v| v.as_str())
13637 .map(String::from),
13638 uri: integ_props
13639 .get("Uri")
13640 .and_then(|v| v.as_str())
13641 .map(String::from),
13642 credentials: integ_props
13643 .get("Credentials")
13644 .and_then(|v| v.as_str())
13645 .map(String::from),
13646 request_parameters: integ_props
13647 .get("RequestParameters")
13648 .and_then(|v| v.as_object())
13649 .map(|obj| {
13650 obj.iter()
13651 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
13652 .collect()
13653 })
13654 .unwrap_or_default(),
13655 request_templates: integ_props
13656 .get("RequestTemplates")
13657 .and_then(|v| v.as_object())
13658 .map(|obj| {
13659 obj.iter()
13660 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
13661 .collect()
13662 })
13663 .unwrap_or_default(),
13664 passthrough_behavior: integ_props
13665 .get("PassthroughBehavior")
13666 .and_then(|v| v.as_str())
13667 .unwrap_or("WHEN_NO_MATCH")
13668 .to_string(),
13669 timeout_in_millis: integ_props
13670 .get("TimeoutInMillis")
13671 .and_then(|v| v.as_i64())
13672 .map(|n| n as i32),
13673 cache_namespace: integ_props
13674 .get("CacheNamespace")
13675 .and_then(|v| v.as_str())
13676 .map(String::from),
13677 cache_key_parameters: integ_props
13678 .get("CacheKeyParameters")
13679 .and_then(|v| v.as_array())
13680 .map(|arr| {
13681 arr.iter()
13682 .filter_map(|v| v.as_str().map(String::from))
13683 .collect()
13684 })
13685 .unwrap_or_default(),
13686 content_handling: integ_props
13687 .get("ContentHandling")
13688 .and_then(|v| v.as_str())
13689 .map(String::from),
13690 connection_type: integ_props
13691 .get("ConnectionType")
13692 .and_then(|v| v.as_str())
13693 .map(String::from),
13694 connection_id: integ_props
13695 .get("ConnectionId")
13696 .and_then(|v| v.as_str())
13697 .map(String::from),
13698 tls_config: integ_props.get("TlsConfig").cloned(),
13699 };
13700 state
13701 .integrations
13702 .insert(composite_key.clone(), integration);
13703 }
13704
13705 Ok(ProvisionResult::new(composite_key.clone())
13706 .with("MethodKey", composite_key)
13707 .with("RestApiId", rest_api_id)
13708 .with("ResourceId", resource_id)
13709 .with("HttpMethod", http_method))
13710 }
13711
13712 fn delete_apigw_method(&self, physical_id: &str) -> Result<(), String> {
13713 let mut accounts = self.apigateway_state.write();
13714 let state = accounts.get_or_create(&self.account_id);
13715 state.methods.remove(physical_id);
13716 state.integrations.remove(physical_id);
13717 let prefix = format!("{physical_id}/");
13718 state
13719 .integration_responses
13720 .retain(|k, _| !k.starts_with(&prefix));
13721 state
13722 .method_responses
13723 .retain(|k, _| !k.starts_with(&prefix));
13724 Ok(())
13725 }
13726
13727 fn create_apigw_deployment(
13728 &self,
13729 resource: &ResourceDefinition,
13730 ) -> Result<ProvisionResult, String> {
13731 let props = &resource.properties;
13732 let rest_api_id = props
13733 .get("RestApiId")
13734 .and_then(|v| v.as_str())
13735 .ok_or("RestApiId is required")?
13736 .to_string();
13737 let description = props
13738 .get("Description")
13739 .and_then(|v| v.as_str())
13740 .map(String::from);
13741
13742 let id = apigw_make_id();
13743 let mut accounts = self.apigateway_state.write();
13744 let state = accounts.get_or_create(&self.account_id);
13745 if !state.apis.contains_key(&rest_api_id) {
13746 return Err(format!("RestApi {rest_api_id} not found"));
13747 }
13748 let api_summary = serde_json::to_value(
13749 state
13750 .resources
13751 .get(&rest_api_id)
13752 .cloned()
13753 .unwrap_or_default(),
13754 )
13755 .unwrap_or(serde_json::Value::Null);
13756 let deployment = ApiGwDeployment {
13757 id: id.clone(),
13758 description,
13759 created_date: Utc::now(),
13760 api_summary,
13761 };
13762 state
13763 .deployments
13764 .entry(rest_api_id.clone())
13765 .or_default()
13766 .insert(id.clone(), deployment);
13767
13768 if let Some(stage_name) = props
13770 .get("StageName")
13771 .and_then(|v| v.as_str())
13772 .map(String::from)
13773 {
13774 let stage = ApiGwStage {
13775 stage_name: stage_name.clone(),
13776 deployment_id: id.clone(),
13777 description: props
13778 .get("StageDescription")
13779 .and_then(|v| v.get("Description"))
13780 .and_then(|v| v.as_str())
13781 .map(String::from),
13782 cache_cluster_enabled: false,
13783 cache_cluster_size: None,
13784 variables: BTreeMap::new(),
13785 method_settings: BTreeMap::new(),
13786 created_date: Utc::now(),
13787 last_updated_date: Utc::now(),
13788 tracing_enabled: false,
13789 web_acl_arn: None,
13790 canary_settings: None,
13791 access_log_settings: None,
13792 tags: BTreeMap::new(),
13793 };
13794 state
13795 .stages
13796 .entry(rest_api_id.clone())
13797 .or_default()
13798 .insert(stage_name, stage);
13799 }
13800
13801 Ok(ProvisionResult::new(id.clone())
13802 .with("DeploymentId", id)
13803 .with("RestApiId", rest_api_id))
13804 }
13805
13806 fn delete_apigw_deployment(
13807 &self,
13808 physical_id: &str,
13809 attributes: &BTreeMap<String, String>,
13810 ) -> Result<(), String> {
13811 let Some(rest_api_id) = attributes.get("RestApiId") else {
13812 return Ok(());
13813 };
13814 let mut accounts = self.apigateway_state.write();
13815 let state = accounts.get_or_create(&self.account_id);
13816 if let Some(map) = state.deployments.get_mut(rest_api_id) {
13817 map.remove(physical_id);
13818 }
13819 Ok(())
13820 }
13821
13822 fn create_apigw_stage(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
13823 let props = &resource.properties;
13824 let rest_api_id = props
13825 .get("RestApiId")
13826 .and_then(|v| v.as_str())
13827 .ok_or("RestApiId is required")?
13828 .to_string();
13829 let stage_name = props
13830 .get("StageName")
13831 .and_then(|v| v.as_str())
13832 .ok_or("StageName is required")?
13833 .to_string();
13834 let deployment_id = props
13835 .get("DeploymentId")
13836 .and_then(|v| v.as_str())
13837 .ok_or("DeploymentId is required")?
13838 .to_string();
13839
13840 let variables: BTreeMap<String, String> = props
13841 .get("Variables")
13842 .and_then(|v| v.as_object())
13843 .map(|obj| {
13844 obj.iter()
13845 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
13846 .collect()
13847 })
13848 .unwrap_or_default();
13849 let tracing_enabled = props
13850 .get("TracingEnabled")
13851 .and_then(|v| v.as_bool())
13852 .unwrap_or(false);
13853 let cache_cluster_enabled = props
13854 .get("CacheClusterEnabled")
13855 .and_then(|v| v.as_bool())
13856 .unwrap_or(false);
13857 let cache_cluster_size = props
13858 .get("CacheClusterSize")
13859 .and_then(|v| v.as_str())
13860 .map(String::from);
13861 let method_settings: BTreeMap<String, serde_json::Value> = props
13865 .get("MethodSettings")
13866 .and_then(|v| v.as_array())
13867 .map(|arr| {
13868 arr.iter()
13869 .filter_map(|s| {
13870 let path = s.get("ResourcePath").and_then(|v| v.as_str())?;
13871 let http = s.get("HttpMethod").and_then(|v| v.as_str())?;
13872 let key = format!("{}/{http}", path.strip_prefix('/').unwrap_or(path));
13873 Some((key, s.clone()))
13874 })
13875 .collect()
13876 })
13877 .unwrap_or_default();
13878 let tags = parse_acm_tags(props.get("Tags"));
13879
13880 let stage = ApiGwStage {
13881 stage_name: stage_name.clone(),
13882 deployment_id,
13883 description: props
13884 .get("Description")
13885 .and_then(|v| v.as_str())
13886 .map(String::from),
13887 cache_cluster_enabled,
13888 cache_cluster_size,
13889 variables,
13890 method_settings,
13891 created_date: Utc::now(),
13892 last_updated_date: Utc::now(),
13893 tracing_enabled,
13894 web_acl_arn: None,
13895 canary_settings: props.get("CanarySetting").cloned(),
13896 access_log_settings: props.get("AccessLogSetting").cloned(),
13897 tags,
13898 };
13899
13900 let mut accounts = self.apigateway_state.write();
13901 let state = accounts.get_or_create(&self.account_id);
13902 if !state.apis.contains_key(&rest_api_id) {
13903 return Err(format!("RestApi {rest_api_id} not found"));
13904 }
13905 let dep_known = state
13906 .deployments
13907 .get(&rest_api_id)
13908 .map(|m| m.contains_key(&stage.deployment_id))
13909 .unwrap_or(false);
13910 if !dep_known {
13911 return Err(format!(
13912 "Deployment {} not yet provisioned for api {rest_api_id}",
13913 stage.deployment_id
13914 ));
13915 }
13916 state
13917 .stages
13918 .entry(rest_api_id.clone())
13919 .or_default()
13920 .insert(stage_name.clone(), stage);
13921
13922 Ok(ProvisionResult::new(stage_name.clone())
13923 .with("StageName", stage_name)
13924 .with("RestApiId", rest_api_id))
13925 }
13926
13927 fn delete_apigw_stage(
13928 &self,
13929 physical_id: &str,
13930 attributes: &BTreeMap<String, String>,
13931 ) -> Result<(), String> {
13932 let Some(rest_api_id) = attributes.get("RestApiId") else {
13933 return Ok(());
13934 };
13935 let mut accounts = self.apigateway_state.write();
13936 let state = accounts.get_or_create(&self.account_id);
13937 if let Some(map) = state.stages.get_mut(rest_api_id) {
13938 map.remove(physical_id);
13939 }
13940 Ok(())
13941 }
13942
13943 fn create_apigw_authorizer(
13944 &self,
13945 resource: &ResourceDefinition,
13946 ) -> Result<ProvisionResult, String> {
13947 let props = &resource.properties;
13948 let rest_api_id = props
13949 .get("RestApiId")
13950 .and_then(|v| v.as_str())
13951 .ok_or("RestApiId is required")?
13952 .to_string();
13953 let name = props
13954 .get("Name")
13955 .and_then(|v| v.as_str())
13956 .ok_or("Name is required")?
13957 .to_string();
13958 let authorizer_type = props
13959 .get("Type")
13960 .and_then(|v| v.as_str())
13961 .unwrap_or("TOKEN")
13962 .to_string();
13963 let provider_arns: Vec<String> = props
13964 .get("ProviderARNs")
13965 .and_then(|v| v.as_array())
13966 .map(|arr| {
13967 arr.iter()
13968 .filter_map(|v| v.as_str().map(String::from))
13969 .collect()
13970 })
13971 .unwrap_or_default();
13972
13973 let id = apigw_make_id();
13974 let auth = ApiGwAuthorizer {
13975 id: id.clone(),
13976 name,
13977 authorizer_type,
13978 provider_arns,
13979 auth_type: props
13980 .get("AuthType")
13981 .and_then(|v| v.as_str())
13982 .map(String::from),
13983 authorizer_uri: props
13984 .get("AuthorizerUri")
13985 .and_then(|v| v.as_str())
13986 .map(String::from),
13987 authorizer_credentials: props
13988 .get("AuthorizerCredentials")
13989 .and_then(|v| v.as_str())
13990 .map(String::from),
13991 identity_source: props
13992 .get("IdentitySource")
13993 .and_then(|v| v.as_str())
13994 .map(String::from),
13995 identity_validation_expression: props
13996 .get("IdentityValidationExpression")
13997 .and_then(|v| v.as_str())
13998 .map(String::from),
13999 authorizer_result_ttl_in_seconds: props
14000 .get("AuthorizerResultTtlInSeconds")
14001 .and_then(|v| v.as_i64())
14002 .map(|n| n as i32),
14003 };
14004
14005 let mut accounts = self.apigateway_state.write();
14006 let state = accounts.get_or_create(&self.account_id);
14007 if !state.apis.contains_key(&rest_api_id) {
14008 return Err(format!("RestApi {rest_api_id} not found"));
14009 }
14010 state
14011 .authorizers
14012 .entry(rest_api_id.clone())
14013 .or_default()
14014 .insert(id.clone(), auth);
14015
14016 Ok(ProvisionResult::new(id.clone())
14017 .with("AuthorizerId", id)
14018 .with("RestApiId", rest_api_id))
14019 }
14020
14021 fn delete_apigw_authorizer(
14022 &self,
14023 physical_id: &str,
14024 attributes: &BTreeMap<String, String>,
14025 ) -> Result<(), String> {
14026 let Some(rest_api_id) = attributes.get("RestApiId") else {
14027 return Ok(());
14028 };
14029 let mut accounts = self.apigateway_state.write();
14030 let state = accounts.get_or_create(&self.account_id);
14031 if let Some(map) = state.authorizers.get_mut(rest_api_id) {
14032 map.remove(physical_id);
14033 }
14034 Ok(())
14035 }
14036
14037 fn create_apigw_request_validator(
14038 &self,
14039 resource: &ResourceDefinition,
14040 ) -> Result<ProvisionResult, String> {
14041 let props = &resource.properties;
14042 let rest_api_id = props
14043 .get("RestApiId")
14044 .and_then(|v| v.as_str())
14045 .ok_or("RestApiId is required")?
14046 .to_string();
14047 let name = props.get("Name").and_then(|v| v.as_str()).map(String::from);
14048 let validate_body = props
14049 .get("ValidateRequestBody")
14050 .and_then(|v| v.as_bool())
14051 .unwrap_or(false);
14052 let validate_params = props
14053 .get("ValidateRequestParameters")
14054 .and_then(|v| v.as_bool())
14055 .unwrap_or(false);
14056 let id = apigw_make_id();
14057 let body = serde_json::json!({
14058 "id": id,
14059 "name": name,
14060 "validateRequestBody": validate_body,
14061 "validateRequestParameters": validate_params,
14062 });
14063 let mut accounts = self.apigateway_state.write();
14064 let state = accounts.get_or_create(&self.account_id);
14065 state
14066 .request_validators
14067 .entry(rest_api_id.clone())
14068 .or_default()
14069 .insert(id.clone(), body);
14070 Ok(ProvisionResult::new(id.clone())
14071 .with("RequestValidatorId", id)
14072 .with("RestApiId", rest_api_id))
14073 }
14074
14075 fn delete_apigw_request_validator(
14076 &self,
14077 physical_id: &str,
14078 attributes: &BTreeMap<String, String>,
14079 ) -> Result<(), String> {
14080 let Some(rest_api_id) = attributes.get("RestApiId") else {
14081 return Ok(());
14082 };
14083 let mut accounts = self.apigateway_state.write();
14084 let state = accounts.get_or_create(&self.account_id);
14085 if let Some(map) = state.request_validators.get_mut(rest_api_id) {
14086 map.remove(physical_id);
14087 }
14088 Ok(())
14089 }
14090
14091 fn create_apigw_model(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
14092 let props = &resource.properties;
14093 let rest_api_id = props
14094 .get("RestApiId")
14095 .and_then(|v| v.as_str())
14096 .ok_or("RestApiId is required")?
14097 .to_string();
14098 let name = props
14099 .get("Name")
14100 .and_then(|v| v.as_str())
14101 .ok_or("Name is required")?
14102 .to_string();
14103 let content_type = props
14104 .get("ContentType")
14105 .and_then(|v| v.as_str())
14106 .unwrap_or("application/json")
14107 .to_string();
14108 let schema = props.get("Schema").map(|v| {
14109 if let Some(s) = v.as_str() {
14110 s.to_string()
14111 } else {
14112 v.to_string()
14113 }
14114 });
14115 let id = apigw_make_id();
14116 let model = ApiGwModel {
14117 id: id.clone(),
14118 name: name.clone(),
14119 description: props
14120 .get("Description")
14121 .and_then(|v| v.as_str())
14122 .map(String::from),
14123 schema,
14124 content_type,
14125 };
14126 let mut accounts = self.apigateway_state.write();
14127 let state = accounts.get_or_create(&self.account_id);
14128 state
14129 .models
14130 .entry(rest_api_id.clone())
14131 .or_default()
14132 .insert(name.clone(), model);
14133 Ok(ProvisionResult::new(name.clone())
14134 .with("ModelName", name)
14135 .with("RestApiId", rest_api_id))
14136 }
14137
14138 fn delete_apigw_model(
14139 &self,
14140 physical_id: &str,
14141 attributes: &BTreeMap<String, String>,
14142 ) -> Result<(), String> {
14143 let Some(rest_api_id) = attributes.get("RestApiId") else {
14144 return Ok(());
14145 };
14146 let mut accounts = self.apigateway_state.write();
14147 let state = accounts.get_or_create(&self.account_id);
14148 if let Some(map) = state.models.get_mut(rest_api_id) {
14149 map.remove(physical_id);
14150 }
14151 Ok(())
14152 }
14153
14154 fn create_apigw_gateway_response(
14155 &self,
14156 resource: &ResourceDefinition,
14157 ) -> Result<ProvisionResult, String> {
14158 let props = &resource.properties;
14159 let rest_api_id = props
14160 .get("RestApiId")
14161 .and_then(|v| v.as_str())
14162 .ok_or("RestApiId is required")?
14163 .to_string();
14164 let response_type = props
14165 .get("ResponseType")
14166 .and_then(|v| v.as_str())
14167 .ok_or("ResponseType is required")?
14168 .to_string();
14169 let body = serde_json::json!({
14170 "responseType": response_type,
14171 "statusCode": props.get("StatusCode").and_then(|v| v.as_str()),
14172 "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
14173 "responseTemplates": props.get("ResponseTemplates").cloned().unwrap_or(serde_json::json!({})),
14174 });
14175 let mut accounts = self.apigateway_state.write();
14176 let state = accounts.get_or_create(&self.account_id);
14177 state
14178 .gateway_responses
14179 .entry(rest_api_id.clone())
14180 .or_default()
14181 .insert(response_type.clone(), body);
14182 Ok(ProvisionResult::new(response_type.clone())
14183 .with("ResponseType", response_type)
14184 .with("RestApiId", rest_api_id))
14185 }
14186
14187 fn delete_apigw_gateway_response(
14188 &self,
14189 physical_id: &str,
14190 attributes: &BTreeMap<String, String>,
14191 ) -> Result<(), String> {
14192 let Some(rest_api_id) = attributes.get("RestApiId") else {
14193 return Ok(());
14194 };
14195 let mut accounts = self.apigateway_state.write();
14196 let state = accounts.get_or_create(&self.account_id);
14197 if let Some(map) = state.gateway_responses.get_mut(rest_api_id) {
14198 map.remove(physical_id);
14199 }
14200 Ok(())
14201 }
14202
14203 fn create_apigw_usage_plan(
14204 &self,
14205 resource: &ResourceDefinition,
14206 ) -> Result<ProvisionResult, String> {
14207 let props = &resource.properties;
14208 let name = props
14209 .get("UsagePlanName")
14210 .and_then(|v| v.as_str())
14211 .ok_or("UsagePlanName is required")?
14212 .to_string();
14213 let id = apigw_make_id();
14214 let plan = ApiGwUsagePlan {
14215 id: id.clone(),
14216 name,
14217 description: props
14218 .get("Description")
14219 .and_then(|v| v.as_str())
14220 .map(String::from),
14221 api_stages: props
14222 .get("ApiStages")
14223 .and_then(|v| v.as_array())
14224 .cloned()
14225 .unwrap_or_default()
14226 .into_iter()
14227 .map(lowercase_first_keys)
14228 .collect(),
14229 throttle: props.get("Throttle").cloned().map(lowercase_first_keys),
14230 quota: props.get("Quota").cloned().map(lowercase_first_keys),
14231 product_code: None,
14232 tags: parse_acm_tags(props.get("Tags")),
14233 };
14234 let mut accounts = self.apigateway_state.write();
14235 let state = accounts.get_or_create(&self.account_id);
14236 state.usage_plans.insert(id.clone(), plan);
14237 Ok(ProvisionResult::new(id.clone()).with("UsagePlanId", id))
14238 }
14239
14240 fn delete_apigw_usage_plan(&self, physical_id: &str) -> Result<(), String> {
14241 let mut accounts = self.apigateway_state.write();
14242 let state = accounts.get_or_create(&self.account_id);
14243 state.usage_plans.remove(physical_id);
14244 state.usage_plan_keys.remove(physical_id);
14245 Ok(())
14246 }
14247
14248 fn create_apigw_api_key(
14249 &self,
14250 resource: &ResourceDefinition,
14251 ) -> Result<ProvisionResult, String> {
14252 let props = &resource.properties;
14253 let generate_distinct_id = props
14254 .get("GenerateDistinctId")
14255 .and_then(|v| v.as_bool())
14256 .unwrap_or(false);
14257 let name = props
14258 .get("Name")
14259 .and_then(|v| v.as_str())
14260 .map(String::from)
14261 .unwrap_or_else(|| {
14262 if generate_distinct_id {
14263 format!("cfn-key-{}-{}", resource.logical_id, apigw_make_id())
14264 } else {
14265 format!("cfn-key-{}", resource.logical_id)
14266 }
14267 });
14268 let value = props
14269 .get("Value")
14270 .and_then(|v| v.as_str())
14271 .map(String::from)
14272 .unwrap_or_else(|| Uuid::new_v4().simple().to_string());
14273 let enabled = props
14274 .get("Enabled")
14275 .and_then(|v| v.as_bool())
14276 .unwrap_or(true);
14277 let stage_keys: Vec<String> = props
14280 .get("StageKeys")
14281 .and_then(|v| v.as_array())
14282 .map(|arr| {
14283 arr.iter()
14284 .filter_map(|s| {
14285 let rest = s.get("RestApiId").and_then(|v| v.as_str())?;
14286 let stage = s.get("StageName").and_then(|v| v.as_str())?;
14287 Some(format!("{rest}/{stage}"))
14288 })
14289 .collect()
14290 })
14291 .unwrap_or_default();
14292 let id = apigw_make_id();
14293 let now = Utc::now();
14294 let key = ApiGwApiKey {
14295 id: id.clone(),
14296 value,
14297 name,
14298 description: props
14299 .get("Description")
14300 .and_then(|v| v.as_str())
14301 .map(String::from),
14302 enabled,
14303 created_date: now,
14304 last_updated_date: now,
14305 stage_keys,
14306 tags: parse_acm_tags(props.get("Tags")),
14307 customer_id: props
14308 .get("CustomerId")
14309 .and_then(|v| v.as_str())
14310 .map(String::from),
14311 };
14312 let mut accounts = self.apigateway_state.write();
14313 let state = accounts.get_or_create(&self.account_id);
14314 state.api_keys.insert(id.clone(), key);
14315 Ok(ProvisionResult::new(id.clone()).with("ApiKeyId", id))
14316 }
14317
14318 fn delete_apigw_api_key(&self, physical_id: &str) -> Result<(), String> {
14319 let mut accounts = self.apigateway_state.write();
14320 let state = accounts.get_or_create(&self.account_id);
14321 state.api_keys.remove(physical_id);
14322 Ok(())
14323 }
14324
14325 fn create_apigw_usage_plan_key(
14326 &self,
14327 resource: &ResourceDefinition,
14328 ) -> Result<ProvisionResult, String> {
14329 let props = &resource.properties;
14330 let usage_plan_id = props
14331 .get("UsagePlanId")
14332 .and_then(|v| v.as_str())
14333 .ok_or("UsagePlanId is required")?
14334 .to_string();
14335 let key_id = props
14336 .get("KeyId")
14337 .and_then(|v| v.as_str())
14338 .ok_or("KeyId is required")?
14339 .to_string();
14340 let key_type = props
14341 .get("KeyType")
14342 .and_then(|v| v.as_str())
14343 .unwrap_or("API_KEY")
14344 .to_string();
14345 let body = serde_json::json!({
14346 "id": key_id,
14347 "type": key_type,
14348 });
14349 let mut accounts = self.apigateway_state.write();
14350 let state = accounts.get_or_create(&self.account_id);
14351 if !state.usage_plans.contains_key(&usage_plan_id) {
14352 return Err(format!("UsagePlan {usage_plan_id} not yet provisioned"));
14353 }
14354 if !state.api_keys.contains_key(&key_id) {
14355 return Err(format!("ApiKey {key_id} not yet provisioned"));
14356 }
14357 state
14358 .usage_plan_keys
14359 .entry(usage_plan_id.clone())
14360 .or_default()
14361 .insert(key_id.clone(), body);
14362 let physical = format!("{usage_plan_id}/{key_id}");
14363 Ok(ProvisionResult::new(physical)
14364 .with("UsagePlanId", usage_plan_id)
14365 .with("KeyId", key_id))
14366 }
14367
14368 fn delete_apigw_usage_plan_key(
14369 &self,
14370 physical_id: &str,
14371 _attributes: &BTreeMap<String, String>,
14372 ) -> Result<(), String> {
14373 let mut parts = physical_id.splitn(2, '/');
14374 let Some(plan_id) = parts.next() else {
14375 return Ok(());
14376 };
14377 let Some(key_id) = parts.next() else {
14378 return Ok(());
14379 };
14380 let mut accounts = self.apigateway_state.write();
14381 let state = accounts.get_or_create(&self.account_id);
14382 if let Some(map) = state.usage_plan_keys.get_mut(plan_id) {
14383 map.remove(key_id);
14384 }
14385 Ok(())
14386 }
14387
14388 fn create_apigw_domain_name(
14389 &self,
14390 resource: &ResourceDefinition,
14391 ) -> Result<ProvisionResult, String> {
14392 let props = &resource.properties;
14393 let domain_name = props
14394 .get("DomainName")
14395 .and_then(|v| v.as_str())
14396 .ok_or("DomainName is required")?
14397 .to_string();
14398 let mtls = props
14399 .get("MutualTlsAuthentication")
14400 .cloned()
14401 .map(lowercase_first_keys);
14402 let regional_domain = format!(
14403 "d-{}.execute-api.{}.amazonaws.com",
14404 apigw_make_id(),
14405 self.region
14406 );
14407 let distribution_domain = format!("d{}.cloudfront.net", apigw_make_id());
14408 let body = serde_json::json!({
14409 "domainName": domain_name,
14410 "certificateArn": props.get("CertificateArn").and_then(|v| v.as_str()),
14411 "regionalCertificateArn": props.get("RegionalCertificateArn").and_then(|v| v.as_str()),
14412 "endpointConfiguration": props.get("EndpointConfiguration").cloned().unwrap_or(serde_json::json!({"types": ["EDGE"]})),
14413 "securityPolicy": props.get("SecurityPolicy").and_then(|v| v.as_str()),
14414 "ownershipVerificationCertificateArn": props.get("OwnershipVerificationCertificateArn").and_then(|v| v.as_str()),
14415 "regionalDomainName": regional_domain,
14416 "regionalHostedZoneId": "Z2FDTNDATAQYW2",
14417 "distributionDomainName": distribution_domain,
14418 "distributionHostedZoneId": "Z2FDTNDATAQYW2",
14419 "mutualTlsAuthentication": mtls,
14420 "tags": serde_json::Value::Object(
14421 parse_acm_tags(props.get("Tags"))
14422 .into_iter()
14423 .map(|(k, v)| (k, serde_json::Value::String(v)))
14424 .collect(),
14425 ),
14426 });
14427 let mut accounts = self.apigateway_state.write();
14428 let state = accounts.get_or_create(&self.account_id);
14429 state.domain_names.insert(domain_name.clone(), body);
14430 Ok(ProvisionResult::new(domain_name.clone())
14431 .with("DomainName", domain_name)
14432 .with("RegionalHostedZoneId", "Z2FDTNDATAQYW2".to_string())
14433 .with("DistributionHostedZoneId", "Z2FDTNDATAQYW2".to_string()))
14434 }
14435
14436 fn delete_apigw_domain_name(&self, physical_id: &str) -> Result<(), String> {
14437 let mut accounts = self.apigateway_state.write();
14438 let state = accounts.get_or_create(&self.account_id);
14439 state.domain_names.remove(physical_id);
14440 state.base_path_mappings.remove(physical_id);
14441 Ok(())
14442 }
14443
14444 fn create_apigw_base_path_mapping(
14445 &self,
14446 resource: &ResourceDefinition,
14447 ) -> Result<ProvisionResult, String> {
14448 let props = &resource.properties;
14449 let domain_name = props
14450 .get("DomainName")
14451 .and_then(|v| v.as_str())
14452 .ok_or("DomainName is required")?
14453 .to_string();
14454 let rest_api_id = props
14455 .get("RestApiId")
14456 .and_then(|v| v.as_str())
14457 .ok_or("RestApiId is required")?
14458 .to_string();
14459 let base_path = props
14460 .get("BasePath")
14461 .and_then(|v| v.as_str())
14462 .unwrap_or("(none)")
14463 .to_string();
14464 let stage = props
14465 .get("Stage")
14466 .and_then(|v| v.as_str())
14467 .map(String::from);
14468 let body = serde_json::json!({
14469 "basePath": base_path,
14470 "restApiId": rest_api_id,
14471 "stage": stage,
14472 });
14473 let mut accounts = self.apigateway_state.write();
14474 let state = accounts.get_or_create(&self.account_id);
14475 state
14476 .base_path_mappings
14477 .entry(domain_name.clone())
14478 .or_default()
14479 .insert(base_path.clone(), body);
14480 let physical = format!("{domain_name}/{base_path}");
14481 Ok(ProvisionResult::new(physical)
14482 .with("DomainName", domain_name)
14483 .with("BasePath", base_path))
14484 }
14485
14486 fn delete_apigw_base_path_mapping(
14487 &self,
14488 physical_id: &str,
14489 _attributes: &BTreeMap<String, String>,
14490 ) -> Result<(), String> {
14491 let mut parts = physical_id.splitn(2, '/');
14492 let Some(domain) = parts.next() else {
14493 return Ok(());
14494 };
14495 let Some(base_path) = parts.next() else {
14496 return Ok(());
14497 };
14498 let mut accounts = self.apigateway_state.write();
14499 let state = accounts.get_or_create(&self.account_id);
14500 if let Some(map) = state.base_path_mappings.get_mut(domain) {
14501 map.remove(base_path);
14502 }
14503 Ok(())
14504 }
14505
14506 fn update_apigw_resource(
14514 &self,
14515 existing: &StackResource,
14516 resource: &ResourceDefinition,
14517 ) -> Result<ProvisionResult, String> {
14518 let props = &resource.properties;
14519 let rest_api_id = existing
14520 .attributes
14521 .get("RestApiId")
14522 .cloned()
14523 .or_else(|| {
14524 props
14525 .get("RestApiId")
14526 .and_then(|v| v.as_str())
14527 .map(String::from)
14528 })
14529 .ok_or("RestApiId is required")?;
14530 let physical = existing.physical_id.clone();
14531 let mut accounts = self.apigateway_state.write();
14532 let state = accounts.get_or_create(&self.account_id);
14533 let api_resources = state
14534 .resources
14535 .get_mut(&rest_api_id)
14536 .ok_or_else(|| format!("RestApi {rest_api_id} not found"))?;
14537 if !api_resources.contains_key(&physical) {
14538 return Err(format!("Resource {physical} not found"));
14539 }
14540 if let Some(part) = props.get("PathPart").and_then(|v| v.as_str()) {
14541 let parent_id = api_resources
14543 .get(&physical)
14544 .and_then(|r| r.parent_id.clone());
14545 let parent_path = parent_id
14546 .as_ref()
14547 .and_then(|pid| api_resources.get(pid).map(|p| p.path.clone()))
14548 .unwrap_or_else(|| "/".to_string());
14549 let new_path = if parent_path == "/" {
14550 format!("/{part}")
14551 } else {
14552 format!("{parent_path}/{part}")
14553 };
14554 let res = api_resources
14555 .get_mut(&physical)
14556 .expect("contains_key checked above");
14557 res.path_part = Some(part.to_string());
14558 res.path = new_path;
14559 }
14560 Ok(ProvisionResult::new(physical.clone())
14561 .with("ResourceId", physical)
14562 .with("RestApiId", rest_api_id))
14563 }
14564
14565 fn update_apigw_method(
14566 &self,
14567 existing: &StackResource,
14568 resource: &ResourceDefinition,
14569 ) -> Result<ProvisionResult, String> {
14570 self.create_apigw_method(resource).map(|r| {
14576 ProvisionResult {
14578 physical_id: existing.physical_id.clone(),
14579 attributes: r.attributes,
14580 }
14581 })
14582 }
14583
14584 fn update_apigw_deployment(
14585 &self,
14586 existing: &StackResource,
14587 resource: &ResourceDefinition,
14588 ) -> Result<ProvisionResult, String> {
14589 let props = &resource.properties;
14590 let rest_api_id = existing
14591 .attributes
14592 .get("RestApiId")
14593 .cloned()
14594 .or_else(|| {
14595 props
14596 .get("RestApiId")
14597 .and_then(|v| v.as_str())
14598 .map(String::from)
14599 })
14600 .ok_or("RestApiId is required")?;
14601 let physical = existing.physical_id.clone();
14602 let mut accounts = self.apigateway_state.write();
14603 let state = accounts.get_or_create(&self.account_id);
14604 let dep = state
14605 .deployments
14606 .get_mut(&rest_api_id)
14607 .and_then(|m| m.get_mut(&physical))
14608 .ok_or_else(|| format!("Deployment {physical} not found"))?;
14609 if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
14610 dep.description = Some(desc.to_string());
14611 }
14612 Ok(ProvisionResult::new(physical.clone())
14613 .with("DeploymentId", physical)
14614 .with("RestApiId", rest_api_id))
14615 }
14616
14617 fn update_apigw_stage(
14618 &self,
14619 existing: &StackResource,
14620 resource: &ResourceDefinition,
14621 ) -> Result<ProvisionResult, String> {
14622 let props = &resource.properties;
14623 let rest_api_id = existing
14624 .attributes
14625 .get("RestApiId")
14626 .cloned()
14627 .or_else(|| {
14628 props
14629 .get("RestApiId")
14630 .and_then(|v| v.as_str())
14631 .map(String::from)
14632 })
14633 .ok_or("RestApiId is required")?;
14634 let stage_name = existing.physical_id.clone();
14635 let mut accounts = self.apigateway_state.write();
14636 let state = accounts.get_or_create(&self.account_id);
14637 let stage = state
14638 .stages
14639 .get_mut(&rest_api_id)
14640 .and_then(|m| m.get_mut(&stage_name))
14641 .ok_or_else(|| format!("Stage {stage_name} not found"))?;
14642 if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
14643 stage.description = Some(desc.to_string());
14644 }
14645 if let Some(b) = props.get("TracingEnabled").and_then(|v| v.as_bool()) {
14646 stage.tracing_enabled = b;
14647 }
14648 if let Some(b) = props.get("CacheClusterEnabled").and_then(|v| v.as_bool()) {
14649 stage.cache_cluster_enabled = b;
14650 }
14651 if let Some(s) = props.get("CacheClusterSize").and_then(|v| v.as_str()) {
14652 stage.cache_cluster_size = Some(s.to_string());
14653 }
14654 if let Some(obj) = props.get("Variables").and_then(|v| v.as_object()) {
14655 stage.variables = obj
14656 .iter()
14657 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
14658 .collect();
14659 }
14660 if let Some(dep) = props.get("DeploymentId").and_then(|v| v.as_str()) {
14661 stage.deployment_id = dep.to_string();
14662 }
14663 if props.get("Tags").is_some() {
14664 stage.tags = parse_acm_tags(props.get("Tags"));
14665 }
14666 if let Some(arr) = props.get("MethodSettings").and_then(|v| v.as_array()) {
14667 stage.method_settings = arr
14668 .iter()
14669 .filter_map(|s| {
14670 let path = s.get("ResourcePath").and_then(|v| v.as_str())?;
14671 let http = s.get("HttpMethod").and_then(|v| v.as_str())?;
14672 let key = format!("{}/{http}", path.strip_prefix('/').unwrap_or(path));
14673 Some((key, s.clone()))
14674 })
14675 .collect();
14676 }
14677 if let Some(canary) = props.get("CanarySetting").cloned() {
14678 stage.canary_settings = Some(canary);
14679 }
14680 if let Some(access) = props.get("AccessLogSetting").cloned() {
14681 stage.access_log_settings = Some(access);
14682 }
14683 stage.last_updated_date = Utc::now();
14684 Ok(ProvisionResult::new(stage_name.clone())
14685 .with("StageName", stage_name)
14686 .with("RestApiId", rest_api_id))
14687 }
14688
14689 fn update_apigw_authorizer(
14690 &self,
14691 existing: &StackResource,
14692 resource: &ResourceDefinition,
14693 ) -> Result<ProvisionResult, String> {
14694 let props = &resource.properties;
14695 let rest_api_id = existing
14696 .attributes
14697 .get("RestApiId")
14698 .cloned()
14699 .or_else(|| {
14700 props
14701 .get("RestApiId")
14702 .and_then(|v| v.as_str())
14703 .map(String::from)
14704 })
14705 .ok_or("RestApiId is required")?;
14706 let physical = existing.physical_id.clone();
14707 let mut accounts = self.apigateway_state.write();
14708 let state = accounts.get_or_create(&self.account_id);
14709 let auth = state
14710 .authorizers
14711 .get_mut(&rest_api_id)
14712 .and_then(|m| m.get_mut(&physical))
14713 .ok_or_else(|| format!("Authorizer {physical} not found"))?;
14714 if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
14715 auth.name = name.to_string();
14716 }
14717 if let Some(t) = props.get("Type").and_then(|v| v.as_str()) {
14718 auth.authorizer_type = t.to_string();
14719 }
14720 if let Some(uri) = props.get("AuthorizerUri").and_then(|v| v.as_str()) {
14721 auth.authorizer_uri = Some(uri.to_string());
14722 }
14723 if let Some(arr) = props.get("ProviderARNs").and_then(|v| v.as_array()) {
14724 auth.provider_arns = arr
14725 .iter()
14726 .filter_map(|v| v.as_str().map(String::from))
14727 .collect();
14728 }
14729 if let Some(s) = props.get("IdentitySource").and_then(|v| v.as_str()) {
14730 auth.identity_source = Some(s.to_string());
14731 }
14732 if let Some(s) = props
14733 .get("IdentityValidationExpression")
14734 .and_then(|v| v.as_str())
14735 {
14736 auth.identity_validation_expression = Some(s.to_string());
14737 }
14738 if let Some(n) = props
14739 .get("AuthorizerResultTtlInSeconds")
14740 .and_then(|v| v.as_i64())
14741 {
14742 auth.authorizer_result_ttl_in_seconds = Some(n as i32);
14743 }
14744 if let Some(s) = props.get("AuthType").and_then(|v| v.as_str()) {
14745 auth.auth_type = Some(s.to_string());
14746 }
14747 if let Some(s) = props.get("AuthorizerCredentials").and_then(|v| v.as_str()) {
14748 auth.authorizer_credentials = Some(s.to_string());
14749 }
14750 Ok(ProvisionResult::new(physical.clone())
14751 .with("AuthorizerId", physical)
14752 .with("RestApiId", rest_api_id))
14753 }
14754
14755 fn update_apigw_request_validator(
14756 &self,
14757 existing: &StackResource,
14758 resource: &ResourceDefinition,
14759 ) -> Result<ProvisionResult, String> {
14760 let props = &resource.properties;
14761 let rest_api_id = existing
14762 .attributes
14763 .get("RestApiId")
14764 .cloned()
14765 .or_else(|| {
14766 props
14767 .get("RestApiId")
14768 .and_then(|v| v.as_str())
14769 .map(String::from)
14770 })
14771 .ok_or("RestApiId is required")?;
14772 let physical = existing.physical_id.clone();
14773 let mut accounts = self.apigateway_state.write();
14774 let state = accounts.get_or_create(&self.account_id);
14775 let body = state
14776 .request_validators
14777 .get_mut(&rest_api_id)
14778 .and_then(|m| m.get_mut(&physical))
14779 .ok_or_else(|| format!("RequestValidator {physical} not found"))?;
14780 let obj = body.as_object_mut().ok_or("validator body not object")?;
14781 if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
14782 obj.insert("name".into(), serde_json::Value::String(name.into()));
14783 }
14784 if let Some(b) = props.get("ValidateRequestBody").and_then(|v| v.as_bool()) {
14785 obj.insert("validateRequestBody".into(), serde_json::Value::Bool(b));
14786 }
14787 if let Some(b) = props
14788 .get("ValidateRequestParameters")
14789 .and_then(|v| v.as_bool())
14790 {
14791 obj.insert(
14792 "validateRequestParameters".into(),
14793 serde_json::Value::Bool(b),
14794 );
14795 }
14796 Ok(ProvisionResult::new(physical.clone())
14797 .with("RequestValidatorId", physical)
14798 .with("RestApiId", rest_api_id))
14799 }
14800
14801 fn update_apigw_model(
14802 &self,
14803 existing: &StackResource,
14804 resource: &ResourceDefinition,
14805 ) -> Result<ProvisionResult, String> {
14806 let props = &resource.properties;
14807 let rest_api_id = existing
14808 .attributes
14809 .get("RestApiId")
14810 .cloned()
14811 .or_else(|| {
14812 props
14813 .get("RestApiId")
14814 .and_then(|v| v.as_str())
14815 .map(String::from)
14816 })
14817 .ok_or("RestApiId is required")?;
14818 let model_name = existing.physical_id.clone();
14819 let mut accounts = self.apigateway_state.write();
14820 let state = accounts.get_or_create(&self.account_id);
14821 let model = state
14822 .models
14823 .get_mut(&rest_api_id)
14824 .and_then(|m| m.get_mut(&model_name))
14825 .ok_or_else(|| format!("Model {model_name} not found"))?;
14826 if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
14827 model.description = Some(desc.to_string());
14828 }
14829 if let Some(s) = props.get("ContentType").and_then(|v| v.as_str()) {
14830 model.content_type = s.to_string();
14831 }
14832 if let Some(schema) = props.get("Schema") {
14833 model.schema = Some(if let Some(s) = schema.as_str() {
14834 s.to_string()
14835 } else {
14836 schema.to_string()
14837 });
14838 }
14839 Ok(ProvisionResult::new(model_name.clone())
14840 .with("ModelName", model_name)
14841 .with("RestApiId", rest_api_id))
14842 }
14843
14844 fn update_apigw_gateway_response(
14845 &self,
14846 existing: &StackResource,
14847 resource: &ResourceDefinition,
14848 ) -> Result<ProvisionResult, String> {
14849 let props = &resource.properties;
14850 let rest_api_id = existing
14851 .attributes
14852 .get("RestApiId")
14853 .cloned()
14854 .or_else(|| {
14855 props
14856 .get("RestApiId")
14857 .and_then(|v| v.as_str())
14858 .map(String::from)
14859 })
14860 .ok_or("RestApiId is required")?;
14861 let response_type = existing.physical_id.clone();
14862 let mut accounts = self.apigateway_state.write();
14863 let state = accounts.get_or_create(&self.account_id);
14864 let body = state
14865 .gateway_responses
14866 .get_mut(&rest_api_id)
14867 .and_then(|m| m.get_mut(&response_type))
14868 .ok_or_else(|| format!("GatewayResponse {response_type} not found"))?;
14869 let obj = body.as_object_mut().ok_or("response body not object")?;
14870 if let Some(s) = props.get("StatusCode").and_then(|v| v.as_str()) {
14871 obj.insert("statusCode".into(), serde_json::Value::String(s.into()));
14872 }
14873 if let Some(v) = props.get("ResponseParameters").cloned() {
14874 obj.insert("responseParameters".into(), v);
14875 }
14876 if let Some(v) = props.get("ResponseTemplates").cloned() {
14877 obj.insert("responseTemplates".into(), v);
14878 }
14879 Ok(ProvisionResult::new(response_type.clone())
14880 .with("ResponseType", response_type)
14881 .with("RestApiId", rest_api_id))
14882 }
14883
14884 fn update_apigw_usage_plan(
14885 &self,
14886 existing: &StackResource,
14887 resource: &ResourceDefinition,
14888 ) -> Result<ProvisionResult, String> {
14889 let props = &resource.properties;
14890 let physical = existing.physical_id.clone();
14891 let mut accounts = self.apigateway_state.write();
14892 let state = accounts.get_or_create(&self.account_id);
14893 let plan = state
14894 .usage_plans
14895 .get_mut(&physical)
14896 .ok_or_else(|| format!("UsagePlan {physical} not found"))?;
14897 if let Some(name) = props.get("UsagePlanName").and_then(|v| v.as_str()) {
14898 plan.name = name.to_string();
14899 }
14900 if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
14901 plan.description = Some(s.to_string());
14902 }
14903 if let Some(arr) = props.get("ApiStages").and_then(|v| v.as_array()) {
14904 plan.api_stages = arr.iter().cloned().map(lowercase_first_keys).collect();
14905 }
14906 if let Some(t) = props.get("Throttle").cloned() {
14907 plan.throttle = Some(lowercase_first_keys(t));
14908 }
14909 if let Some(q) = props.get("Quota").cloned() {
14910 plan.quota = Some(lowercase_first_keys(q));
14911 }
14912 if props.get("Tags").is_some() {
14913 plan.tags = parse_acm_tags(props.get("Tags"));
14914 }
14915 Ok(ProvisionResult::new(physical.clone()).with("UsagePlanId", physical))
14916 }
14917
14918 fn update_apigw_api_key(
14919 &self,
14920 existing: &StackResource,
14921 resource: &ResourceDefinition,
14922 ) -> Result<ProvisionResult, String> {
14923 let props = &resource.properties;
14924 let physical = existing.physical_id.clone();
14925 let mut accounts = self.apigateway_state.write();
14926 let state = accounts.get_or_create(&self.account_id);
14927 let key = state
14928 .api_keys
14929 .get_mut(&physical)
14930 .ok_or_else(|| format!("ApiKey {physical} not found"))?;
14931 if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
14932 key.name = name.to_string();
14933 }
14934 if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
14935 key.description = Some(s.to_string());
14936 }
14937 if let Some(b) = props.get("Enabled").and_then(|v| v.as_bool()) {
14938 key.enabled = b;
14939 }
14940 if let Some(s) = props.get("CustomerId").and_then(|v| v.as_str()) {
14941 key.customer_id = Some(s.to_string());
14942 }
14943 if props.get("Tags").is_some() {
14944 key.tags = parse_acm_tags(props.get("Tags"));
14945 }
14946 if let Some(arr) = props.get("StageKeys").and_then(|v| v.as_array()) {
14947 key.stage_keys = arr
14948 .iter()
14949 .filter_map(|s| {
14950 let rest = s.get("RestApiId").and_then(|v| v.as_str())?;
14951 let stage = s.get("StageName").and_then(|v| v.as_str())?;
14952 Some(format!("{rest}/{stage}"))
14953 })
14954 .collect();
14955 }
14956 key.last_updated_date = Utc::now();
14957 Ok(ProvisionResult::new(physical.clone()).with("ApiKeyId", physical))
14958 }
14959
14960 fn update_apigw_usage_plan_key(
14961 &self,
14962 existing: &StackResource,
14963 _resource: &ResourceDefinition,
14964 ) -> Result<ProvisionResult, String> {
14965 let physical = existing.physical_id.clone();
14970 let mut parts = physical.splitn(2, '/');
14971 let plan = parts.next().unwrap_or("").to_string();
14972 let key = parts.next().unwrap_or("").to_string();
14973 Ok(ProvisionResult::new(physical)
14974 .with("UsagePlanId", plan)
14975 .with("KeyId", key))
14976 }
14977
14978 fn update_apigw_domain_name(
14979 &self,
14980 existing: &StackResource,
14981 resource: &ResourceDefinition,
14982 ) -> Result<ProvisionResult, String> {
14983 let props = &resource.properties;
14984 let domain = existing.physical_id.clone();
14985 let mut accounts = self.apigateway_state.write();
14986 let state = accounts.get_or_create(&self.account_id);
14987 let body = state
14988 .domain_names
14989 .get_mut(&domain)
14990 .ok_or_else(|| format!("DomainName {domain} not found"))?;
14991 let obj = body.as_object_mut().ok_or("domain body not object")?;
14992 if let Some(s) = props.get("CertificateArn").and_then(|v| v.as_str()) {
14993 obj.insert("certificateArn".into(), serde_json::Value::String(s.into()));
14994 }
14995 if let Some(s) = props.get("RegionalCertificateArn").and_then(|v| v.as_str()) {
14996 obj.insert(
14997 "regionalCertificateArn".into(),
14998 serde_json::Value::String(s.into()),
14999 );
15000 }
15001 if let Some(v) = props.get("EndpointConfiguration").cloned() {
15002 obj.insert("endpointConfiguration".into(), v);
15003 }
15004 if let Some(s) = props.get("SecurityPolicy").and_then(|v| v.as_str()) {
15005 obj.insert("securityPolicy".into(), serde_json::Value::String(s.into()));
15006 }
15007 if let Some(v) = props.get("MutualTlsAuthentication").cloned() {
15008 obj.insert("mutualTlsAuthentication".into(), lowercase_first_keys(v));
15009 }
15010 if let Some(s) = props
15011 .get("OwnershipVerificationCertificateArn")
15012 .and_then(|v| v.as_str())
15013 {
15014 obj.insert(
15015 "ownershipVerificationCertificateArn".into(),
15016 serde_json::Value::String(s.into()),
15017 );
15018 }
15019 if props.get("Tags").is_some() {
15020 obj.insert(
15021 "tags".into(),
15022 serde_json::Value::Object(
15023 parse_acm_tags(props.get("Tags"))
15024 .into_iter()
15025 .map(|(k, v)| (k, serde_json::Value::String(v)))
15026 .collect(),
15027 ),
15028 );
15029 }
15030 Ok(ProvisionResult::new(domain.clone())
15031 .with("DomainName", domain)
15032 .with("RegionalHostedZoneId", "Z2FDTNDATAQYW2".to_string())
15033 .with("DistributionHostedZoneId", "Z2FDTNDATAQYW2".to_string()))
15034 }
15035
15036 fn update_apigw_base_path_mapping(
15037 &self,
15038 existing: &StackResource,
15039 resource: &ResourceDefinition,
15040 ) -> Result<ProvisionResult, String> {
15041 let props = &resource.properties;
15042 let physical = existing.physical_id.clone();
15043 let mut parts = physical.splitn(2, '/');
15044 let domain = parts
15045 .next()
15046 .ok_or("malformed base path mapping id")?
15047 .to_string();
15048 let base_path = parts
15049 .next()
15050 .ok_or("malformed base path mapping id")?
15051 .to_string();
15052 let mut accounts = self.apigateway_state.write();
15053 let state = accounts.get_or_create(&self.account_id);
15054 let map = state
15055 .base_path_mappings
15056 .get_mut(&domain)
15057 .ok_or_else(|| format!("DomainName {domain} not found"))?;
15058 let body = map
15059 .get_mut(&base_path)
15060 .ok_or_else(|| format!("BasePath {base_path} not found"))?;
15061 let obj = body.as_object_mut().ok_or("mapping body not object")?;
15062 if let Some(s) = props.get("RestApiId").and_then(|v| v.as_str()) {
15063 obj.insert("restApiId".into(), serde_json::Value::String(s.into()));
15064 }
15065 if let Some(s) = props.get("Stage").and_then(|v| v.as_str()) {
15066 obj.insert("stage".into(), serde_json::Value::String(s.into()));
15067 }
15068 Ok(ProvisionResult::new(physical)
15069 .with("DomainName", domain)
15070 .with("BasePath", base_path))
15071 }
15072
15073 fn create_apigwv2_api(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
15076 let props = &resource.properties;
15077 let name = props
15078 .get("Name")
15079 .and_then(|v| v.as_str())
15080 .ok_or("Name is required")?
15081 .to_string();
15082 let protocol_type = props
15083 .get("ProtocolType")
15084 .and_then(|v| v.as_str())
15085 .unwrap_or("HTTP")
15086 .to_string();
15087 let description = props
15088 .get("Description")
15089 .and_then(|v| v.as_str())
15090 .map(String::from);
15091 let tags: Option<BTreeMap<String, String>> =
15092 props.get("Tags").and_then(|v| v.as_object()).map(|obj| {
15093 obj.iter()
15094 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
15095 .collect()
15096 });
15097
15098 let id = make_apigwv2_id(10);
15099 let mut api = ApiGwV2HttpApi::new(id.clone(), name, description, tags, &self.region);
15100 api.protocol_type = protocol_type.clone();
15101 if let Some(expr) = props
15102 .get("RouteSelectionExpression")
15103 .and_then(|v| v.as_str())
15104 {
15105 api.route_selection_expression = expr.to_string();
15106 }
15107 if let Some(expr) = props
15108 .get("ApiKeySelectionExpression")
15109 .and_then(|v| v.as_str())
15110 {
15111 api.api_key_selection_expression = expr.to_string();
15112 }
15113 if let Some(b) = props
15114 .get("DisableExecuteApiEndpoint")
15115 .and_then(|v| v.as_bool())
15116 {
15117 api.disable_execute_api_endpoint = b;
15118 }
15119 if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
15120 api.ip_address_type = s.to_string();
15121 }
15122 if let Some(cors) = props.get("CorsConfiguration").and_then(|v| v.as_object()) {
15123 api.cors_configuration = Some(ApiGwV2CorsConfiguration {
15124 allow_credentials: cors.get("AllowCredentials").and_then(|v| v.as_bool()),
15125 allow_headers: cors
15126 .get("AllowHeaders")
15127 .and_then(|v| v.as_array())
15128 .map(|a| {
15129 a.iter()
15130 .filter_map(|v| v.as_str().map(String::from))
15131 .collect()
15132 }),
15133 allow_methods: cors
15134 .get("AllowMethods")
15135 .and_then(|v| v.as_array())
15136 .map(|a| {
15137 a.iter()
15138 .filter_map(|v| v.as_str().map(String::from))
15139 .collect()
15140 }),
15141 allow_origins: cors
15142 .get("AllowOrigins")
15143 .and_then(|v| v.as_array())
15144 .map(|a| {
15145 a.iter()
15146 .filter_map(|v| v.as_str().map(String::from))
15147 .collect()
15148 }),
15149 expose_headers: cors
15150 .get("ExposeHeaders")
15151 .and_then(|v| v.as_array())
15152 .map(|a| {
15153 a.iter()
15154 .filter_map(|v| v.as_str().map(String::from))
15155 .collect()
15156 }),
15157 max_age: cors
15158 .get("MaxAge")
15159 .and_then(|v| v.as_i64())
15160 .map(|n| n as i32),
15161 });
15162 }
15163
15164 let api_endpoint = api.api_endpoint.clone();
15165 let mut accounts = self.apigatewayv2_state.write();
15166 let state = accounts.get_or_create(&self.account_id);
15167 state.apis.insert(id.clone(), api);
15168
15169 Ok(ProvisionResult::new(id.clone())
15170 .with("ApiId", id)
15171 .with("ApiEndpoint", api_endpoint))
15172 }
15173
15174 fn delete_apigwv2_api(&self, physical_id: &str) -> Result<(), String> {
15175 let mut accounts = self.apigatewayv2_state.write();
15176 let state = accounts.get_or_create(&self.account_id);
15177 state.apis.remove(physical_id);
15178 state.routes.remove(physical_id);
15179 state.integrations.remove(physical_id);
15180 state.stages.remove(physical_id);
15181 state.deployments.remove(physical_id);
15182 state.authorizers.remove(physical_id);
15183 state.models.remove(physical_id);
15184 state.integration_responses.remove(physical_id);
15185 state.route_responses.remove(physical_id);
15186 Ok(())
15187 }
15188
15189 fn create_apigwv2_route(
15190 &self,
15191 resource: &ResourceDefinition,
15192 ) -> Result<ProvisionResult, String> {
15193 let props = &resource.properties;
15194 let api_id = props
15195 .get("ApiId")
15196 .and_then(|v| v.as_str())
15197 .ok_or("ApiId is required")?
15198 .to_string();
15199 let route_key = props
15200 .get("RouteKey")
15201 .and_then(|v| v.as_str())
15202 .ok_or("RouteKey is required")?
15203 .to_string();
15204
15205 let mut accounts = self.apigatewayv2_state.write();
15206 let state = accounts.get_or_create(&self.account_id);
15207 if !state.apis.contains_key(&api_id) {
15208 return Err(format!("Api {api_id} not yet provisioned"));
15209 }
15210 let id = make_apigwv2_id(10);
15211 let route = ApiGwV2Route {
15212 route_id: id.clone(),
15213 route_key,
15214 target: props
15215 .get("Target")
15216 .and_then(|v| v.as_str())
15217 .map(String::from),
15218 authorization_type: props
15219 .get("AuthorizationType")
15220 .and_then(|v| v.as_str())
15221 .map(String::from),
15222 authorizer_id: props
15223 .get("AuthorizerId")
15224 .and_then(|v| v.as_str())
15225 .map(String::from),
15226 };
15227 state
15228 .routes
15229 .entry(api_id.clone())
15230 .or_default()
15231 .insert(id.clone(), route);
15232
15233 Ok(ProvisionResult::new(id.clone())
15234 .with("RouteId", id)
15235 .with("ApiId", api_id))
15236 }
15237
15238 fn delete_apigwv2_route(
15239 &self,
15240 physical_id: &str,
15241 attributes: &BTreeMap<String, String>,
15242 ) -> Result<(), String> {
15243 let Some(api_id) = attributes.get("ApiId") else {
15244 return Ok(());
15245 };
15246 let mut accounts = self.apigatewayv2_state.write();
15247 let state = accounts.get_or_create(&self.account_id);
15248 if let Some(map) = state.routes.get_mut(api_id) {
15249 map.remove(physical_id);
15250 }
15251 Ok(())
15252 }
15253
15254 fn create_apigwv2_integration(
15255 &self,
15256 resource: &ResourceDefinition,
15257 ) -> Result<ProvisionResult, String> {
15258 let props = &resource.properties;
15259 let api_id = props
15260 .get("ApiId")
15261 .and_then(|v| v.as_str())
15262 .ok_or("ApiId is required")?
15263 .to_string();
15264 let integration_type = props
15265 .get("IntegrationType")
15266 .and_then(|v| v.as_str())
15267 .ok_or("IntegrationType is required")?
15268 .to_string();
15269
15270 let mut accounts = self.apigatewayv2_state.write();
15271 let state = accounts.get_or_create(&self.account_id);
15272 if !state.apis.contains_key(&api_id) {
15273 return Err(format!("Api {api_id} not yet provisioned"));
15274 }
15275 let id = make_apigwv2_id(10);
15276 let integration = ApiGwV2Integration {
15277 integration_id: id.clone(),
15278 integration_type,
15279 integration_uri: props
15280 .get("IntegrationUri")
15281 .and_then(|v| v.as_str())
15282 .map(String::from),
15283 payload_format_version: props
15284 .get("PayloadFormatVersion")
15285 .and_then(|v| v.as_str())
15286 .map(String::from),
15287 timeout_in_millis: props.get("TimeoutInMillis").and_then(|v| v.as_i64()),
15288 };
15289 state
15290 .integrations
15291 .entry(api_id.clone())
15292 .or_default()
15293 .insert(id.clone(), integration);
15294
15295 Ok(ProvisionResult::new(id.clone())
15296 .with("IntegrationId", id)
15297 .with("ApiId", api_id))
15298 }
15299
15300 fn delete_apigwv2_integration(
15301 &self,
15302 physical_id: &str,
15303 attributes: &BTreeMap<String, String>,
15304 ) -> Result<(), String> {
15305 let Some(api_id) = attributes.get("ApiId") else {
15306 return Ok(());
15307 };
15308 let mut accounts = self.apigatewayv2_state.write();
15309 let state = accounts.get_or_create(&self.account_id);
15310 if let Some(map) = state.integrations.get_mut(api_id) {
15311 map.remove(physical_id);
15312 }
15313 Ok(())
15314 }
15315
15316 fn create_apigwv2_integration_response(
15317 &self,
15318 resource: &ResourceDefinition,
15319 ) -> Result<ProvisionResult, String> {
15320 let props = &resource.properties;
15321 let api_id = props
15322 .get("ApiId")
15323 .and_then(|v| v.as_str())
15324 .ok_or("ApiId is required")?
15325 .to_string();
15326 let integration_id = props
15327 .get("IntegrationId")
15328 .and_then(|v| v.as_str())
15329 .ok_or("IntegrationId is required")?
15330 .to_string();
15331 let key_expr = props
15332 .get("IntegrationResponseKey")
15333 .and_then(|v| v.as_str())
15334 .ok_or("IntegrationResponseKey is required")?
15335 .to_string();
15336 let id = make_apigwv2_id(10);
15337 let body = serde_json::json!({
15338 "integrationResponseId": id,
15339 "integrationId": integration_id,
15340 "integrationResponseKey": key_expr,
15341 "responseTemplates": props.get("ResponseTemplates").cloned().unwrap_or(serde_json::json!({})),
15342 "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
15343 "templateSelectionExpression": props.get("TemplateSelectionExpression").and_then(|v| v.as_str()),
15344 "contentHandlingStrategy": props.get("ContentHandlingStrategy").and_then(|v| v.as_str()),
15345 });
15346 let composite_key = format!("{integration_id}/{id}");
15347 let mut accounts = self.apigatewayv2_state.write();
15348 let state = accounts.get_or_create(&self.account_id);
15349 if !state
15350 .integrations
15351 .get(&api_id)
15352 .map(|m| m.contains_key(&integration_id))
15353 .unwrap_or(false)
15354 {
15355 return Err(format!(
15356 "Integration {integration_id} not yet provisioned for api {api_id}"
15357 ));
15358 }
15359 state
15360 .integration_responses
15361 .entry(api_id.clone())
15362 .or_default()
15363 .insert(composite_key.clone(), body);
15364 Ok(ProvisionResult::new(composite_key.clone())
15365 .with("IntegrationResponseId", id)
15366 .with("IntegrationId", integration_id)
15367 .with("ApiId", api_id))
15368 }
15369
15370 fn delete_apigwv2_integration_response(
15371 &self,
15372 physical_id: &str,
15373 attributes: &BTreeMap<String, String>,
15374 ) -> Result<(), String> {
15375 let Some(api_id) = attributes.get("ApiId") else {
15376 return Ok(());
15377 };
15378 let mut accounts = self.apigatewayv2_state.write();
15379 let state = accounts.get_or_create(&self.account_id);
15380 if let Some(map) = state.integration_responses.get_mut(api_id) {
15381 map.remove(physical_id);
15382 }
15383 Ok(())
15384 }
15385
15386 fn create_apigwv2_route_response(
15387 &self,
15388 resource: &ResourceDefinition,
15389 ) -> Result<ProvisionResult, String> {
15390 let props = &resource.properties;
15391 let api_id = props
15392 .get("ApiId")
15393 .and_then(|v| v.as_str())
15394 .ok_or("ApiId is required")?
15395 .to_string();
15396 let route_id = props
15397 .get("RouteId")
15398 .and_then(|v| v.as_str())
15399 .ok_or("RouteId is required")?
15400 .to_string();
15401 let key_expr = props
15402 .get("RouteResponseKey")
15403 .and_then(|v| v.as_str())
15404 .ok_or("RouteResponseKey is required")?
15405 .to_string();
15406 let id = make_apigwv2_id(10);
15407 let body = serde_json::json!({
15408 "routeResponseId": id,
15409 "routeId": route_id,
15410 "routeResponseKey": key_expr,
15411 "responseModels": props.get("ResponseModels").cloned().unwrap_or(serde_json::json!({})),
15412 "modelSelectionExpression": props.get("ModelSelectionExpression").and_then(|v| v.as_str()),
15413 "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
15414 });
15415 let composite = format!("{route_id}/{id}");
15416 let mut accounts = self.apigatewayv2_state.write();
15417 let state = accounts.get_or_create(&self.account_id);
15418 if !state
15419 .routes
15420 .get(&api_id)
15421 .map(|m| m.contains_key(&route_id))
15422 .unwrap_or(false)
15423 {
15424 return Err(format!(
15425 "Route {route_id} not yet provisioned for api {api_id}"
15426 ));
15427 }
15428 state
15429 .route_responses
15430 .entry(api_id.clone())
15431 .or_default()
15432 .insert(composite.clone(), body);
15433 Ok(ProvisionResult::new(composite.clone())
15434 .with("RouteResponseId", id)
15435 .with("RouteId", route_id)
15436 .with("ApiId", api_id))
15437 }
15438
15439 fn delete_apigwv2_route_response(
15440 &self,
15441 physical_id: &str,
15442 attributes: &BTreeMap<String, String>,
15443 ) -> Result<(), String> {
15444 let Some(api_id) = attributes.get("ApiId") else {
15445 return Ok(());
15446 };
15447 let mut accounts = self.apigatewayv2_state.write();
15448 let state = accounts.get_or_create(&self.account_id);
15449 if let Some(map) = state.route_responses.get_mut(api_id) {
15450 map.remove(physical_id);
15451 }
15452 Ok(())
15453 }
15454
15455 fn create_apigwv2_stage(
15456 &self,
15457 resource: &ResourceDefinition,
15458 ) -> Result<ProvisionResult, String> {
15459 let props = &resource.properties;
15460 let api_id = props
15461 .get("ApiId")
15462 .and_then(|v| v.as_str())
15463 .ok_or("ApiId is required")?
15464 .to_string();
15465 let stage_name = props
15466 .get("StageName")
15467 .and_then(|v| v.as_str())
15468 .ok_or("StageName is required")?
15469 .to_string();
15470 let auto_deploy = props
15471 .get("AutoDeploy")
15472 .and_then(|v| v.as_bool())
15473 .unwrap_or(false);
15474 let deployment_id = props
15475 .get("DeploymentId")
15476 .and_then(|v| v.as_str())
15477 .map(String::from);
15478
15479 let stage_variables = props
15480 .get("StageVariables")
15481 .and_then(|v| v.as_object())
15482 .map(|obj| {
15483 obj.iter()
15484 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
15485 .collect()
15486 });
15487
15488 let access_log_settings = props.get("AccessLogSettings").and_then(|v| {
15489 let destination_arn = v.get("DestinationArn")?.as_str()?.to_string();
15490 let format = v.get("Format").and_then(|f| f.as_str().map(String::from));
15491 Some(fakecloud_apigatewayv2::AccessLogSettings {
15492 destination_arn,
15493 format,
15494 })
15495 });
15496
15497 let stage = ApiGwV2Stage {
15498 stage_name: stage_name.clone(),
15499 description: props
15500 .get("Description")
15501 .and_then(|v| v.as_str())
15502 .map(String::from),
15503 deployment_id: deployment_id.clone(),
15504 auto_deploy,
15505 created_date: Utc::now(),
15506 last_updated_date: None,
15507 web_acl_arn: None,
15508 stage_variables,
15509 access_log_settings,
15510 };
15511
15512 let mut accounts = self.apigatewayv2_state.write();
15513 let state = accounts.get_or_create(&self.account_id);
15514 if !state.apis.contains_key(&api_id) {
15515 return Err(format!("Api {api_id} not yet provisioned"));
15516 }
15517 if let Some(dep) = &deployment_id {
15518 if !state
15519 .deployments
15520 .get(&api_id)
15521 .map(|m| m.contains_key(dep))
15522 .unwrap_or(false)
15523 {
15524 return Err(format!(
15525 "Deployment {dep} not yet provisioned for api {api_id}"
15526 ));
15527 }
15528 }
15529 state
15530 .stages
15531 .entry(api_id.clone())
15532 .or_default()
15533 .insert(stage_name.clone(), stage);
15534
15535 Ok(ProvisionResult::new(stage_name.clone())
15536 .with("StageName", stage_name)
15537 .with("ApiId", api_id))
15538 }
15539
15540 fn delete_apigwv2_stage(
15541 &self,
15542 physical_id: &str,
15543 attributes: &BTreeMap<String, String>,
15544 ) -> Result<(), String> {
15545 let Some(api_id) = attributes.get("ApiId") else {
15546 return Ok(());
15547 };
15548 let mut accounts = self.apigatewayv2_state.write();
15549 let state = accounts.get_or_create(&self.account_id);
15550 if let Some(map) = state.stages.get_mut(api_id) {
15551 map.remove(physical_id);
15552 }
15553 Ok(())
15554 }
15555
15556 fn create_apigwv2_deployment(
15557 &self,
15558 resource: &ResourceDefinition,
15559 ) -> Result<ProvisionResult, String> {
15560 let props = &resource.properties;
15561 let api_id = props
15562 .get("ApiId")
15563 .and_then(|v| v.as_str())
15564 .ok_or("ApiId is required")?
15565 .to_string();
15566 let id = make_apigwv2_id(10);
15567 let deployment = ApiGwV2Deployment {
15568 deployment_id: id.clone(),
15569 description: props
15570 .get("Description")
15571 .and_then(|v| v.as_str())
15572 .map(String::from),
15573 created_date: Utc::now(),
15574 auto_deployed: false,
15575 };
15576 let mut accounts = self.apigatewayv2_state.write();
15577 let state = accounts.get_or_create(&self.account_id);
15578 if !state.apis.contains_key(&api_id) {
15579 return Err(format!("Api {api_id} not yet provisioned"));
15580 }
15581 state
15582 .deployments
15583 .entry(api_id.clone())
15584 .or_default()
15585 .insert(id.clone(), deployment);
15586 Ok(ProvisionResult::new(id.clone())
15587 .with("DeploymentId", id)
15588 .with("ApiId", api_id))
15589 }
15590
15591 fn delete_apigwv2_deployment(
15592 &self,
15593 physical_id: &str,
15594 attributes: &BTreeMap<String, String>,
15595 ) -> Result<(), String> {
15596 let Some(api_id) = attributes.get("ApiId") else {
15597 return Ok(());
15598 };
15599 let mut accounts = self.apigatewayv2_state.write();
15600 let state = accounts.get_or_create(&self.account_id);
15601 if let Some(map) = state.deployments.get_mut(api_id) {
15602 map.remove(physical_id);
15603 }
15604 Ok(())
15605 }
15606
15607 fn create_apigwv2_authorizer(
15608 &self,
15609 resource: &ResourceDefinition,
15610 ) -> Result<ProvisionResult, String> {
15611 let props = &resource.properties;
15612 let api_id = props
15613 .get("ApiId")
15614 .and_then(|v| v.as_str())
15615 .ok_or("ApiId is required")?
15616 .to_string();
15617 let name = props
15618 .get("Name")
15619 .and_then(|v| v.as_str())
15620 .ok_or("Name is required")?
15621 .to_string();
15622 let authorizer_type = props
15623 .get("AuthorizerType")
15624 .and_then(|v| v.as_str())
15625 .unwrap_or("REQUEST")
15626 .to_string();
15627 let identity_source = props
15628 .get("IdentitySource")
15629 .and_then(|v| v.as_array())
15630 .map(|arr| {
15631 arr.iter()
15632 .filter_map(|v| v.as_str().map(String::from))
15633 .collect::<Vec<String>>()
15634 });
15635 let jwt_configuration = props
15636 .get("JwtConfiguration")
15637 .and_then(|v| v.as_object())
15638 .map(|obj| ApiGwV2JwtConfiguration {
15639 audience: obj.get("Audience").and_then(|v| v.as_array()).map(|a| {
15640 a.iter()
15641 .filter_map(|v| v.as_str().map(String::from))
15642 .collect()
15643 }),
15644 issuer: obj.get("Issuer").and_then(|v| v.as_str()).map(String::from),
15645 });
15646
15647 let id = make_apigwv2_id(10);
15648 let auth = ApiGwV2Authorizer {
15649 authorizer_id: id.clone(),
15650 name,
15651 authorizer_type,
15652 authorizer_uri: props
15653 .get("AuthorizerUri")
15654 .and_then(|v| v.as_str())
15655 .map(String::from),
15656 identity_source,
15657 jwt_configuration,
15658 };
15659 let mut accounts = self.apigatewayv2_state.write();
15660 let state = accounts.get_or_create(&self.account_id);
15661 if !state.apis.contains_key(&api_id) {
15662 return Err(format!("Api {api_id} not yet provisioned"));
15663 }
15664 state
15665 .authorizers
15666 .entry(api_id.clone())
15667 .or_default()
15668 .insert(id.clone(), auth);
15669 Ok(ProvisionResult::new(id.clone())
15670 .with("AuthorizerId", id)
15671 .with("ApiId", api_id))
15672 }
15673
15674 fn delete_apigwv2_authorizer(
15675 &self,
15676 physical_id: &str,
15677 attributes: &BTreeMap<String, String>,
15678 ) -> Result<(), String> {
15679 let Some(api_id) = attributes.get("ApiId") else {
15680 return Ok(());
15681 };
15682 let mut accounts = self.apigatewayv2_state.write();
15683 let state = accounts.get_or_create(&self.account_id);
15684 if let Some(map) = state.authorizers.get_mut(api_id) {
15685 map.remove(physical_id);
15686 }
15687 Ok(())
15688 }
15689
15690 fn create_apigwv2_domain_name(
15691 &self,
15692 resource: &ResourceDefinition,
15693 ) -> Result<ProvisionResult, String> {
15694 let props = &resource.properties;
15695 let domain_name = props
15696 .get("DomainName")
15697 .and_then(|v| v.as_str())
15698 .ok_or("DomainName is required")?
15699 .to_string();
15700 let body = serde_json::json!({
15701 "domainName": domain_name,
15702 "domainNameConfigurations": props.get("DomainNameConfigurations").cloned().unwrap_or(serde_json::json!([])),
15703 "mutualTlsAuthentication": props.get("MutualTlsAuthentication").cloned(),
15704 "apiMappingSelectionExpression": null,
15705 });
15706 let mut accounts = self.apigatewayv2_state.write();
15707 let state = accounts.get_or_create(&self.account_id);
15708 state.domain_names.insert(domain_name.clone(), body);
15709 Ok(ProvisionResult::new(domain_name.clone()).with("DomainName", domain_name))
15710 }
15711
15712 fn delete_apigwv2_domain_name(&self, physical_id: &str) -> Result<(), String> {
15713 let mut accounts = self.apigatewayv2_state.write();
15714 let state = accounts.get_or_create(&self.account_id);
15715 state.domain_names.remove(physical_id);
15716 state.api_mappings.remove(physical_id);
15717 Ok(())
15718 }
15719
15720 fn create_apigwv2_api_mapping(
15721 &self,
15722 resource: &ResourceDefinition,
15723 ) -> Result<ProvisionResult, String> {
15724 let props = &resource.properties;
15725 let domain_name = props
15726 .get("DomainName")
15727 .and_then(|v| v.as_str())
15728 .ok_or("DomainName is required")?
15729 .to_string();
15730 let api_id = props
15731 .get("ApiId")
15732 .and_then(|v| v.as_str())
15733 .ok_or("ApiId is required")?
15734 .to_string();
15735 let stage = props
15736 .get("Stage")
15737 .and_then(|v| v.as_str())
15738 .ok_or("Stage is required")?
15739 .to_string();
15740 let api_mapping_key = props
15741 .get("ApiMappingKey")
15742 .and_then(|v| v.as_str())
15743 .map(String::from);
15744 let id = make_apigwv2_id(10);
15745 let body = serde_json::json!({
15746 "apiMappingId": id,
15747 "apiId": api_id,
15748 "stage": stage,
15749 "apiMappingKey": api_mapping_key,
15750 });
15751 let mut accounts = self.apigatewayv2_state.write();
15752 let state = accounts.get_or_create(&self.account_id);
15753 if !state.domain_names.contains_key(&domain_name) {
15754 return Err(format!("DomainName {domain_name} not yet provisioned"));
15755 }
15756 if !state.apis.contains_key(&api_id) {
15757 return Err(format!("Api {api_id} not yet provisioned"));
15758 }
15759 state
15760 .api_mappings
15761 .entry(domain_name.clone())
15762 .or_default()
15763 .insert(id.clone(), body);
15764 Ok(ProvisionResult::new(id.clone())
15765 .with("ApiMappingId", id)
15766 .with("DomainName", domain_name))
15767 }
15768
15769 fn delete_apigwv2_api_mapping(
15770 &self,
15771 physical_id: &str,
15772 attributes: &BTreeMap<String, String>,
15773 ) -> Result<(), String> {
15774 let Some(domain) = attributes.get("DomainName") else {
15775 return Ok(());
15776 };
15777 let mut accounts = self.apigatewayv2_state.write();
15778 let state = accounts.get_or_create(&self.account_id);
15779 if let Some(map) = state.api_mappings.get_mut(domain) {
15780 map.remove(physical_id);
15781 }
15782 Ok(())
15783 }
15784
15785 fn create_apigwv2_vpc_link(
15786 &self,
15787 resource: &ResourceDefinition,
15788 ) -> Result<ProvisionResult, String> {
15789 let props = &resource.properties;
15790 let name = props
15791 .get("Name")
15792 .and_then(|v| v.as_str())
15793 .ok_or("Name is required")?
15794 .to_string();
15795 let id = make_apigwv2_id(10);
15796 let body = serde_json::json!({
15797 "vpcLinkId": id,
15798 "name": name,
15799 "subnetIds": props.get("SubnetIds").cloned().unwrap_or(serde_json::json!([])),
15800 "securityGroupIds": props.get("SecurityGroupIds").cloned().unwrap_or(serde_json::json!([])),
15801 "tags": props.get("Tags").cloned().unwrap_or(serde_json::json!({})),
15802 "vpcLinkStatus": "AVAILABLE",
15803 "vpcLinkVersion": "V2",
15804 "createdDate": Utc::now().to_rfc3339(),
15805 });
15806 let mut accounts = self.apigatewayv2_state.write();
15807 let state = accounts.get_or_create(&self.account_id);
15808 state.vpc_links.insert(id.clone(), body);
15809 Ok(ProvisionResult::new(id.clone()).with("VpcLinkId", id))
15810 }
15811
15812 fn delete_apigwv2_vpc_link(&self, physical_id: &str) -> Result<(), String> {
15813 let mut accounts = self.apigatewayv2_state.write();
15814 let state = accounts.get_or_create(&self.account_id);
15815 state.vpc_links.remove(physical_id);
15816 Ok(())
15817 }
15818
15819 fn create_apigwv2_model(
15820 &self,
15821 resource: &ResourceDefinition,
15822 ) -> Result<ProvisionResult, String> {
15823 let props = &resource.properties;
15824 let api_id = props
15825 .get("ApiId")
15826 .and_then(|v| v.as_str())
15827 .ok_or("ApiId is required")?
15828 .to_string();
15829 let name = props
15830 .get("Name")
15831 .and_then(|v| v.as_str())
15832 .ok_or("Name is required")?
15833 .to_string();
15834 let id = make_apigwv2_id(10);
15835 let body = serde_json::json!({
15836 "modelId": id,
15837 "name": name,
15838 "contentType": props.get("ContentType").and_then(|v| v.as_str()).unwrap_or("application/json"),
15839 "description": props.get("Description").and_then(|v| v.as_str()),
15840 "schema": props.get("Schema").map(|v| if let Some(s) = v.as_str() { s.to_string() } else { v.to_string() }),
15841 });
15842 let mut accounts = self.apigatewayv2_state.write();
15843 let state = accounts.get_or_create(&self.account_id);
15844 if !state.apis.contains_key(&api_id) {
15845 return Err(format!("Api {api_id} not yet provisioned"));
15846 }
15847 state
15848 .models
15849 .entry(api_id.clone())
15850 .or_default()
15851 .insert(id.clone(), body);
15852 Ok(ProvisionResult::new(id.clone())
15853 .with("ModelId", id)
15854 .with("ApiId", api_id))
15855 }
15856
15857 fn delete_apigwv2_model(
15858 &self,
15859 physical_id: &str,
15860 attributes: &BTreeMap<String, String>,
15861 ) -> Result<(), String> {
15862 let Some(api_id) = attributes.get("ApiId") else {
15863 return Ok(());
15864 };
15865 let mut accounts = self.apigatewayv2_state.write();
15866 let state = accounts.get_or_create(&self.account_id);
15867 if let Some(map) = state.models.get_mut(api_id) {
15868 map.remove(physical_id);
15869 }
15870 Ok(())
15871 }
15872
15873 fn update_apigwv2_api(
15877 &self,
15878 existing: &StackResource,
15879 resource: &ResourceDefinition,
15880 ) -> Result<ProvisionResult, String> {
15881 let props = &resource.properties;
15882 let api_id = existing.physical_id.clone();
15883 let mut accounts = self.apigatewayv2_state.write();
15884 let state = accounts.get_or_create(&self.account_id);
15885 let api = state
15886 .apis
15887 .get_mut(&api_id)
15888 .ok_or_else(|| format!("Api {api_id} no longer exists in state"))?;
15889
15890 if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
15891 api.name = s.to_string();
15892 }
15893 if let Some(s) = props.get("ProtocolType").and_then(|v| v.as_str()) {
15894 api.protocol_type = s.to_string();
15895 }
15896 api.description = props
15897 .get("Description")
15898 .and_then(|v| v.as_str())
15899 .map(String::from)
15900 .or_else(|| api.description.clone());
15901 if let Some(s) = props
15902 .get("RouteSelectionExpression")
15903 .and_then(|v| v.as_str())
15904 {
15905 api.route_selection_expression = s.to_string();
15906 }
15907 if let Some(s) = props
15908 .get("ApiKeySelectionExpression")
15909 .and_then(|v| v.as_str())
15910 {
15911 api.api_key_selection_expression = s.to_string();
15912 }
15913 if let Some(b) = props
15914 .get("DisableExecuteApiEndpoint")
15915 .and_then(|v| v.as_bool())
15916 {
15917 api.disable_execute_api_endpoint = b;
15918 }
15919 if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
15920 api.ip_address_type = s.to_string();
15921 }
15922 if let Some(cors) = props.get("CorsConfiguration").and_then(|v| v.as_object()) {
15923 api.cors_configuration = Some(ApiGwV2CorsConfiguration {
15924 allow_credentials: cors.get("AllowCredentials").and_then(|v| v.as_bool()),
15925 allow_headers: cors
15926 .get("AllowHeaders")
15927 .and_then(|v| v.as_array())
15928 .map(|a| {
15929 a.iter()
15930 .filter_map(|v| v.as_str().map(String::from))
15931 .collect()
15932 }),
15933 allow_methods: cors
15934 .get("AllowMethods")
15935 .and_then(|v| v.as_array())
15936 .map(|a| {
15937 a.iter()
15938 .filter_map(|v| v.as_str().map(String::from))
15939 .collect()
15940 }),
15941 allow_origins: cors
15942 .get("AllowOrigins")
15943 .and_then(|v| v.as_array())
15944 .map(|a| {
15945 a.iter()
15946 .filter_map(|v| v.as_str().map(String::from))
15947 .collect()
15948 }),
15949 expose_headers: cors
15950 .get("ExposeHeaders")
15951 .and_then(|v| v.as_array())
15952 .map(|a| {
15953 a.iter()
15954 .filter_map(|v| v.as_str().map(String::from))
15955 .collect()
15956 }),
15957 max_age: cors
15958 .get("MaxAge")
15959 .and_then(|v| v.as_i64())
15960 .map(|n| n as i32),
15961 });
15962 }
15963 if let Some(obj) = props.get("Tags").and_then(|v| v.as_object()) {
15964 let tags: BTreeMap<String, String> = obj
15965 .iter()
15966 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
15967 .collect();
15968 api.tags = Some(tags);
15969 }
15970
15971 let api_endpoint = api.api_endpoint.clone();
15972 Ok(ProvisionResult::new(api_id.clone())
15973 .with("ApiId", api_id)
15974 .with("ApiEndpoint", api_endpoint))
15975 }
15976
15977 fn update_apigwv2_route(
15981 &self,
15982 existing: &StackResource,
15983 resource: &ResourceDefinition,
15984 ) -> Result<ProvisionResult, String> {
15985 let props = &resource.properties;
15986 let api_id = props
15987 .get("ApiId")
15988 .and_then(|v| v.as_str())
15989 .ok_or("ApiId is required")?
15990 .to_string();
15991 let route_id = existing.physical_id.clone();
15992
15993 let mut accounts = self.apigatewayv2_state.write();
15994 let state = accounts.get_or_create(&self.account_id);
15995 let routes = state
15996 .routes
15997 .get_mut(&api_id)
15998 .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
15999 let route = routes
16000 .get_mut(&route_id)
16001 .ok_or_else(|| format!("Route {route_id} not yet provisioned for api {api_id}"))?;
16002 if let Some(s) = props.get("RouteKey").and_then(|v| v.as_str()) {
16003 route.route_key = s.to_string();
16004 }
16005 if let Some(s) = props.get("Target").and_then(|v| v.as_str()) {
16006 route.target = Some(s.to_string());
16007 }
16008 if let Some(s) = props.get("AuthorizationType").and_then(|v| v.as_str()) {
16009 route.authorization_type = Some(s.to_string());
16010 }
16011 if let Some(s) = props.get("AuthorizerId").and_then(|v| v.as_str()) {
16012 route.authorizer_id = Some(s.to_string());
16013 }
16014 Ok(ProvisionResult::new(route_id.clone())
16015 .with("RouteId", route_id)
16016 .with("ApiId", api_id))
16017 }
16018
16019 fn update_apigwv2_integration(
16021 &self,
16022 existing: &StackResource,
16023 resource: &ResourceDefinition,
16024 ) -> Result<ProvisionResult, String> {
16025 let props = &resource.properties;
16026 let api_id = props
16027 .get("ApiId")
16028 .and_then(|v| v.as_str())
16029 .ok_or("ApiId is required")?
16030 .to_string();
16031 let integration_id = existing.physical_id.clone();
16032
16033 let mut accounts = self.apigatewayv2_state.write();
16034 let state = accounts.get_or_create(&self.account_id);
16035 let integrations = state
16036 .integrations
16037 .get_mut(&api_id)
16038 .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16039 let integ = integrations.get_mut(&integration_id).ok_or_else(|| {
16040 format!("Integration {integration_id} not yet provisioned for api {api_id}")
16041 })?;
16042 if let Some(s) = props.get("IntegrationType").and_then(|v| v.as_str()) {
16043 integ.integration_type = s.to_string();
16044 }
16045 if let Some(s) = props.get("IntegrationUri").and_then(|v| v.as_str()) {
16046 integ.integration_uri = Some(s.to_string());
16047 }
16048 if let Some(s) = props.get("PayloadFormatVersion").and_then(|v| v.as_str()) {
16049 integ.payload_format_version = Some(s.to_string());
16050 }
16051 if let Some(n) = props.get("TimeoutInMillis").and_then(|v| v.as_i64()) {
16052 integ.timeout_in_millis = Some(n);
16053 }
16054 Ok(ProvisionResult::new(integration_id.clone())
16055 .with("IntegrationId", integration_id)
16056 .with("ApiId", api_id))
16057 }
16058
16059 fn update_apigwv2_integration_response(
16063 &self,
16064 existing: &StackResource,
16065 resource: &ResourceDefinition,
16066 ) -> Result<ProvisionResult, String> {
16067 let props = &resource.properties;
16068 let api_id = props
16069 .get("ApiId")
16070 .and_then(|v| v.as_str())
16071 .ok_or("ApiId is required")?
16072 .to_string();
16073 let composite_key = existing.physical_id.clone();
16074 let (integration_id, response_id) = composite_key
16075 .split_once('/')
16076 .map(|(a, b)| (a.to_string(), b.to_string()))
16077 .ok_or_else(|| format!("Invalid IntegrationResponse physical id: {composite_key}"))?;
16078 let key_expr = props
16079 .get("IntegrationResponseKey")
16080 .and_then(|v| v.as_str())
16081 .ok_or("IntegrationResponseKey is required")?
16082 .to_string();
16083 let body = serde_json::json!({
16084 "integrationResponseId": response_id,
16085 "integrationId": integration_id,
16086 "integrationResponseKey": key_expr,
16087 "responseTemplates": props.get("ResponseTemplates").cloned().unwrap_or(serde_json::json!({})),
16088 "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
16089 "templateSelectionExpression": props.get("TemplateSelectionExpression").and_then(|v| v.as_str()),
16090 "contentHandlingStrategy": props.get("ContentHandlingStrategy").and_then(|v| v.as_str()),
16091 });
16092 let mut accounts = self.apigatewayv2_state.write();
16093 let state = accounts.get_or_create(&self.account_id);
16094 let map = state
16095 .integration_responses
16096 .get_mut(&api_id)
16097 .ok_or_else(|| format!("No integration responses found for api {api_id}"))?;
16098 if !map.contains_key(&composite_key) {
16099 return Err(format!(
16100 "IntegrationResponse {composite_key} not yet provisioned for api {api_id}"
16101 ));
16102 }
16103 map.insert(composite_key.clone(), body);
16104 Ok(ProvisionResult::new(composite_key)
16105 .with("IntegrationResponseId", response_id)
16106 .with("IntegrationId", integration_id)
16107 .with("ApiId", api_id))
16108 }
16109
16110 fn update_apigwv2_route_response(
16113 &self,
16114 existing: &StackResource,
16115 resource: &ResourceDefinition,
16116 ) -> Result<ProvisionResult, String> {
16117 let props = &resource.properties;
16118 let api_id = props
16119 .get("ApiId")
16120 .and_then(|v| v.as_str())
16121 .ok_or("ApiId is required")?
16122 .to_string();
16123 let composite = existing.physical_id.clone();
16124 let (route_id, response_id) = composite
16125 .split_once('/')
16126 .map(|(a, b)| (a.to_string(), b.to_string()))
16127 .ok_or_else(|| format!("Invalid RouteResponse physical id: {composite}"))?;
16128 let key_expr = props
16129 .get("RouteResponseKey")
16130 .and_then(|v| v.as_str())
16131 .ok_or("RouteResponseKey is required")?
16132 .to_string();
16133 let body = serde_json::json!({
16134 "routeResponseId": response_id,
16135 "routeId": route_id,
16136 "routeResponseKey": key_expr,
16137 "responseModels": props.get("ResponseModels").cloned().unwrap_or(serde_json::json!({})),
16138 "modelSelectionExpression": props.get("ModelSelectionExpression").and_then(|v| v.as_str()),
16139 "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
16140 });
16141 let mut accounts = self.apigatewayv2_state.write();
16142 let state = accounts.get_or_create(&self.account_id);
16143 let map = state
16144 .route_responses
16145 .get_mut(&api_id)
16146 .ok_or_else(|| format!("No route responses found for api {api_id}"))?;
16147 if !map.contains_key(&composite) {
16148 return Err(format!(
16149 "RouteResponse {composite} not yet provisioned for api {api_id}"
16150 ));
16151 }
16152 map.insert(composite.clone(), body);
16153 Ok(ProvisionResult::new(composite)
16154 .with("RouteResponseId", response_id)
16155 .with("RouteId", route_id)
16156 .with("ApiId", api_id))
16157 }
16158
16159 fn update_apigwv2_stage(
16163 &self,
16164 existing: &StackResource,
16165 resource: &ResourceDefinition,
16166 ) -> Result<ProvisionResult, String> {
16167 let props = &resource.properties;
16168 let api_id = props
16169 .get("ApiId")
16170 .and_then(|v| v.as_str())
16171 .ok_or("ApiId is required")?
16172 .to_string();
16173 let stage_name = existing.physical_id.clone();
16174
16175 let mut accounts = self.apigatewayv2_state.write();
16176 let state = accounts.get_or_create(&self.account_id);
16177 let stages = state
16178 .stages
16179 .get_mut(&api_id)
16180 .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16181 let stage = stages
16182 .get_mut(&stage_name)
16183 .ok_or_else(|| format!("Stage {stage_name} not yet provisioned for api {api_id}"))?;
16184 if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
16185 stage.description = Some(s.to_string());
16186 }
16187 if let Some(s) = props.get("DeploymentId").and_then(|v| v.as_str()) {
16188 stage.deployment_id = Some(s.to_string());
16189 }
16190 if let Some(b) = props.get("AutoDeploy").and_then(|v| v.as_bool()) {
16191 stage.auto_deploy = b;
16192 }
16193 stage.last_updated_date = Some(Utc::now());
16194 Ok(ProvisionResult::new(stage_name.clone())
16195 .with("StageName", stage_name)
16196 .with("ApiId", api_id))
16197 }
16198
16199 fn update_apigwv2_deployment(
16202 &self,
16203 existing: &StackResource,
16204 resource: &ResourceDefinition,
16205 ) -> Result<ProvisionResult, String> {
16206 let props = &resource.properties;
16207 let api_id = props
16208 .get("ApiId")
16209 .and_then(|v| v.as_str())
16210 .ok_or("ApiId is required")?
16211 .to_string();
16212 let deployment_id = existing.physical_id.clone();
16213 let mut accounts = self.apigatewayv2_state.write();
16214 let state = accounts.get_or_create(&self.account_id);
16215 let deps = state
16216 .deployments
16217 .get_mut(&api_id)
16218 .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16219 let dep = deps.get_mut(&deployment_id).ok_or_else(|| {
16220 format!("Deployment {deployment_id} not yet provisioned for api {api_id}")
16221 })?;
16222 if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
16223 dep.description = Some(s.to_string());
16224 }
16225 Ok(ProvisionResult::new(deployment_id.clone())
16226 .with("DeploymentId", deployment_id)
16227 .with("ApiId", api_id))
16228 }
16229
16230 fn update_apigwv2_authorizer(
16233 &self,
16234 existing: &StackResource,
16235 resource: &ResourceDefinition,
16236 ) -> Result<ProvisionResult, String> {
16237 let props = &resource.properties;
16238 let api_id = props
16239 .get("ApiId")
16240 .and_then(|v| v.as_str())
16241 .ok_or("ApiId is required")?
16242 .to_string();
16243 let authorizer_id = existing.physical_id.clone();
16244
16245 let mut accounts = self.apigatewayv2_state.write();
16246 let state = accounts.get_or_create(&self.account_id);
16247 let auths = state
16248 .authorizers
16249 .get_mut(&api_id)
16250 .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16251 let auth = auths.get_mut(&authorizer_id).ok_or_else(|| {
16252 format!("Authorizer {authorizer_id} not yet provisioned for api {api_id}")
16253 })?;
16254 if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
16255 auth.name = s.to_string();
16256 }
16257 if let Some(s) = props.get("AuthorizerType").and_then(|v| v.as_str()) {
16258 auth.authorizer_type = s.to_string();
16259 }
16260 if let Some(s) = props.get("AuthorizerUri").and_then(|v| v.as_str()) {
16261 auth.authorizer_uri = Some(s.to_string());
16262 }
16263 if let Some(arr) = props.get("IdentitySource").and_then(|v| v.as_array()) {
16264 auth.identity_source = Some(
16265 arr.iter()
16266 .filter_map(|v| v.as_str().map(String::from))
16267 .collect(),
16268 );
16269 }
16270 if let Some(obj) = props.get("JwtConfiguration").and_then(|v| v.as_object()) {
16271 auth.jwt_configuration = Some(ApiGwV2JwtConfiguration {
16272 audience: obj.get("Audience").and_then(|v| v.as_array()).map(|a| {
16273 a.iter()
16274 .filter_map(|v| v.as_str().map(String::from))
16275 .collect()
16276 }),
16277 issuer: obj.get("Issuer").and_then(|v| v.as_str()).map(String::from),
16278 });
16279 }
16280 Ok(ProvisionResult::new(authorizer_id.clone())
16281 .with("AuthorizerId", authorizer_id)
16282 .with("ApiId", api_id))
16283 }
16284
16285 fn update_apigwv2_domain_name(
16289 &self,
16290 existing: &StackResource,
16291 resource: &ResourceDefinition,
16292 ) -> Result<ProvisionResult, String> {
16293 let props = &resource.properties;
16294 let domain_name = existing.physical_id.clone();
16295 let body = serde_json::json!({
16296 "domainName": domain_name,
16297 "domainNameConfigurations": props.get("DomainNameConfigurations").cloned().unwrap_or(serde_json::json!([])),
16298 "mutualTlsAuthentication": props.get("MutualTlsAuthentication").cloned(),
16299 "apiMappingSelectionExpression": null,
16300 });
16301 let mut accounts = self.apigatewayv2_state.write();
16302 let state = accounts.get_or_create(&self.account_id);
16303 if !state.domain_names.contains_key(&domain_name) {
16304 return Err(format!("DomainName {domain_name} no longer exists"));
16305 }
16306 state.domain_names.insert(domain_name.clone(), body);
16307 Ok(ProvisionResult::new(domain_name.clone()).with("DomainName", domain_name))
16308 }
16309
16310 fn update_apigwv2_api_mapping(
16313 &self,
16314 existing: &StackResource,
16315 resource: &ResourceDefinition,
16316 ) -> Result<ProvisionResult, String> {
16317 let props = &resource.properties;
16318 let domain_name = props
16319 .get("DomainName")
16320 .and_then(|v| v.as_str())
16321 .ok_or("DomainName is required")?
16322 .to_string();
16323 let api_id = props
16324 .get("ApiId")
16325 .and_then(|v| v.as_str())
16326 .ok_or("ApiId is required")?
16327 .to_string();
16328 let stage = props
16329 .get("Stage")
16330 .and_then(|v| v.as_str())
16331 .ok_or("Stage is required")?
16332 .to_string();
16333 let api_mapping_key = props
16334 .get("ApiMappingKey")
16335 .and_then(|v| v.as_str())
16336 .map(String::from);
16337 let id = existing.physical_id.clone();
16338 let body = serde_json::json!({
16339 "apiMappingId": id,
16340 "apiId": api_id,
16341 "stage": stage,
16342 "apiMappingKey": api_mapping_key,
16343 });
16344 let mut accounts = self.apigatewayv2_state.write();
16345 let state = accounts.get_or_create(&self.account_id);
16346 let map = state
16347 .api_mappings
16348 .get_mut(&domain_name)
16349 .ok_or_else(|| format!("DomainName {domain_name} no longer exists"))?;
16350 map.insert(id.clone(), body);
16351 Ok(ProvisionResult::new(id.clone())
16352 .with("ApiMappingId", id)
16353 .with("DomainName", domain_name))
16354 }
16355
16356 fn update_apigwv2_vpc_link(
16360 &self,
16361 existing: &StackResource,
16362 resource: &ResourceDefinition,
16363 ) -> Result<ProvisionResult, String> {
16364 let props = &resource.properties;
16365 let id = existing.physical_id.clone();
16366 let mut accounts = self.apigatewayv2_state.write();
16367 let state = accounts.get_or_create(&self.account_id);
16368 let body = state
16369 .vpc_links
16370 .get_mut(&id)
16371 .ok_or_else(|| format!("VpcLink {id} no longer exists"))?;
16372 if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
16373 body["name"] = serde_json::Value::String(s.to_string());
16374 }
16375 if let Some(v) = props.get("SubnetIds").cloned() {
16376 body["subnetIds"] = v;
16377 }
16378 if let Some(v) = props.get("SecurityGroupIds").cloned() {
16379 body["securityGroupIds"] = v;
16380 }
16381 if let Some(v) = props.get("Tags").cloned() {
16382 body["tags"] = v;
16383 }
16384 Ok(ProvisionResult::new(id.clone()).with("VpcLinkId", id))
16385 }
16386
16387 fn update_apigwv2_model(
16391 &self,
16392 existing: &StackResource,
16393 resource: &ResourceDefinition,
16394 ) -> Result<ProvisionResult, String> {
16395 let props = &resource.properties;
16396 let api_id = props
16397 .get("ApiId")
16398 .and_then(|v| v.as_str())
16399 .ok_or("ApiId is required")?
16400 .to_string();
16401 let id = existing.physical_id.clone();
16402 let mut accounts = self.apigatewayv2_state.write();
16403 let state = accounts.get_or_create(&self.account_id);
16404 let map = state
16405 .models
16406 .get_mut(&api_id)
16407 .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16408 let body = map
16409 .get_mut(&id)
16410 .ok_or_else(|| format!("Model {id} not yet provisioned for api {api_id}"))?;
16411 if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
16412 body["name"] = serde_json::Value::String(s.to_string());
16413 }
16414 if let Some(s) = props.get("ContentType").and_then(|v| v.as_str()) {
16415 body["contentType"] = serde_json::Value::String(s.to_string());
16416 }
16417 if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
16418 body["description"] = serde_json::Value::String(s.to_string());
16419 }
16420 if let Some(v) = props.get("Schema") {
16421 body["schema"] = serde_json::Value::String(if let Some(s) = v.as_str() {
16422 s.to_string()
16423 } else {
16424 v.to_string()
16425 });
16426 }
16427 Ok(ProvisionResult::new(id.clone())
16428 .with("ModelId", id)
16429 .with("ApiId", api_id))
16430 }
16431
16432 fn create_ses_configuration_set(
16435 &self,
16436 resource: &ResourceDefinition,
16437 ) -> Result<ProvisionResult, String> {
16438 let props = &resource.properties;
16439 let name = props
16440 .get("Name")
16441 .and_then(|v| v.as_str())
16442 .map(String::from)
16443 .unwrap_or_else(|| format!("cfn-cs-{}", resource.logical_id));
16444 let sending_enabled = props
16445 .get("SendingOptions")
16446 .and_then(|v| v.get("SendingEnabled"))
16447 .and_then(|v| v.as_bool())
16448 .unwrap_or(true);
16449 let tls_policy = props
16450 .get("DeliveryOptions")
16451 .and_then(|v| v.get("TlsPolicy"))
16452 .and_then(|v| v.as_str())
16453 .unwrap_or("OPTIONAL")
16454 .to_string();
16455 let sending_pool_name = props
16456 .get("DeliveryOptions")
16457 .and_then(|v| v.get("SendingPoolName"))
16458 .and_then(|v| v.as_str())
16459 .map(String::from);
16460 let custom_redirect_domain = props
16461 .get("TrackingOptions")
16462 .and_then(|v| v.get("CustomRedirectDomain"))
16463 .and_then(|v| v.as_str())
16464 .map(String::from);
16465 let suppressed_reasons: Vec<String> = props
16466 .get("SuppressionOptions")
16467 .and_then(|v| v.get("SuppressedReasons"))
16468 .and_then(|v| v.as_array())
16469 .map(|arr| {
16470 arr.iter()
16471 .filter_map(|v| v.as_str().map(String::from))
16472 .collect()
16473 })
16474 .unwrap_or_default();
16475 let reputation_metrics_enabled = props
16476 .get("ReputationOptions")
16477 .and_then(|v| v.get("ReputationMetricsEnabled"))
16478 .and_then(|v| v.as_bool())
16479 .unwrap_or(false);
16480
16481 let cs = SesConfigurationSet {
16482 name: name.clone(),
16483 sending_enabled,
16484 tls_policy,
16485 sending_pool_name,
16486 custom_redirect_domain,
16487 https_policy: props
16488 .get("TrackingOptions")
16489 .and_then(|v| v.get("HttpsPolicy"))
16490 .and_then(|v| v.as_str())
16491 .map(String::from),
16492 suppressed_reasons,
16493 reputation_metrics_enabled,
16494 vdm_options: props.get("VdmOptions").cloned(),
16495 archive_arn: props
16496 .get("ArchivingOptions")
16497 .and_then(|v| v.get("ArchiveArn"))
16498 .and_then(|v| v.as_str())
16499 .map(String::from),
16500 };
16501 let mut accounts = self.ses_state.write();
16502 let state = accounts.get_or_create(&self.account_id);
16503 state.configuration_sets.insert(name.clone(), cs);
16504 Ok(ProvisionResult::new(name.clone()).with("Name", name))
16505 }
16506
16507 fn delete_ses_configuration_set(&self, physical_id: &str) -> Result<(), String> {
16508 let mut accounts = self.ses_state.write();
16509 let state = accounts.get_or_create(&self.account_id);
16510 state.configuration_sets.remove(physical_id);
16511 state.event_destinations.remove(physical_id);
16512 Ok(())
16513 }
16514
16515 fn create_ses_event_destination(
16516 &self,
16517 resource: &ResourceDefinition,
16518 ) -> Result<ProvisionResult, String> {
16519 let props = &resource.properties;
16520 let cs_name = props
16521 .get("ConfigurationSetName")
16522 .and_then(|v| v.as_str())
16523 .ok_or("ConfigurationSetName is required")?
16524 .to_string();
16525 let dest_props = props
16526 .get("EventDestination")
16527 .and_then(|v| v.as_object())
16528 .ok_or("EventDestination is required")?;
16529 let name = dest_props
16530 .get("Name")
16531 .and_then(|v| v.as_str())
16532 .map(String::from)
16533 .unwrap_or_else(|| format!("cfn-ed-{}", resource.logical_id));
16534 let enabled = dest_props
16535 .get("Enabled")
16536 .and_then(|v| v.as_bool())
16537 .unwrap_or(true);
16538 let matching_event_types: Vec<String> = dest_props
16539 .get("MatchingEventTypes")
16540 .and_then(|v| v.as_array())
16541 .map(|arr| {
16542 arr.iter()
16543 .filter_map(|v| v.as_str().map(String::from))
16544 .collect()
16545 })
16546 .unwrap_or_default();
16547 let dest = SesEventDestination {
16548 name: name.clone(),
16549 enabled,
16550 matching_event_types,
16551 kinesis_firehose_destination: dest_props.get("KinesisFirehoseDestination").cloned(),
16552 cloud_watch_destination: dest_props.get("CloudWatchDestination").cloned(),
16553 sns_destination: dest_props.get("SnsDestination").cloned(),
16554 event_bridge_destination: dest_props.get("EventBridgeDestination").cloned(),
16555 pinpoint_destination: dest_props.get("PinpointDestination").cloned(),
16556 };
16557 let mut accounts = self.ses_state.write();
16558 let state = accounts.get_or_create(&self.account_id);
16559 if !state.configuration_sets.contains_key(&cs_name) {
16560 return Err(format!("ConfigurationSet {cs_name} not yet provisioned"));
16561 }
16562 let dests = state.event_destinations.entry(cs_name.clone()).or_default();
16563 dests.retain(|d| d.name != name);
16564 dests.push(dest);
16565 let physical = format!("{cs_name}|{name}");
16566 Ok(ProvisionResult::new(physical)
16567 .with("Name", name)
16568 .with("ConfigurationSetName", cs_name))
16569 }
16570
16571 fn delete_ses_event_destination(
16572 &self,
16573 physical_id: &str,
16574 _attributes: &BTreeMap<String, String>,
16575 ) -> Result<(), String> {
16576 let mut parts = physical_id.splitn(2, '|');
16577 let Some(cs) = parts.next() else {
16578 return Ok(());
16579 };
16580 let Some(name) = parts.next() else {
16581 return Ok(());
16582 };
16583 let mut accounts = self.ses_state.write();
16584 let state = accounts.get_or_create(&self.account_id);
16585 if let Some(dests) = state.event_destinations.get_mut(cs) {
16586 dests.retain(|d| d.name != name);
16587 }
16588 Ok(())
16589 }
16590
16591 fn create_ses_email_identity(
16592 &self,
16593 resource: &ResourceDefinition,
16594 ) -> Result<ProvisionResult, String> {
16595 let props = &resource.properties;
16596 let identity_name = props
16597 .get("EmailIdentity")
16598 .and_then(|v| v.as_str())
16599 .ok_or("EmailIdentity is required")?
16600 .to_string();
16601 let identity_type = if identity_name.contains('@') {
16602 "EMAIL_ADDRESS"
16603 } else {
16604 "DOMAIN"
16605 }
16606 .to_string();
16607 let dkim_signing_enabled = props
16608 .get("DkimAttributes")
16609 .and_then(|v| v.get("SigningEnabled"))
16610 .and_then(|v| v.as_bool())
16611 .unwrap_or(true);
16612 let dkim_signing_attributes_origin = props
16613 .get("DkimSigningAttributes")
16614 .map(|_| "EXTERNAL")
16615 .unwrap_or("AWS_SES")
16616 .to_string();
16617 let mail_from_domain = props
16618 .get("MailFromAttributes")
16619 .and_then(|v| v.get("MailFromDomain"))
16620 .and_then(|v| v.as_str())
16621 .map(String::from);
16622 let mail_from_behavior = props
16623 .get("MailFromAttributes")
16624 .and_then(|v| v.get("BehaviorOnMxFailure"))
16625 .and_then(|v| v.as_str())
16626 .unwrap_or("USE_DEFAULT_VALUE")
16627 .to_string();
16628 let configuration_set_name = props
16629 .get("ConfigurationSetAttributes")
16630 .and_then(|v| v.get("ConfigurationSetName"))
16631 .and_then(|v| v.as_str())
16632 .map(String::from);
16633 let email_forwarding_enabled = props
16634 .get("FeedbackAttributes")
16635 .and_then(|v| v.get("EmailForwardingEnabled"))
16636 .and_then(|v| v.as_bool())
16637 .unwrap_or(true);
16638
16639 let identity = SesEmailIdentity {
16640 identity_name: identity_name.clone(),
16641 identity_type,
16642 verified: true,
16643 created_at: Utc::now(),
16644 dkim_signing_enabled,
16645 dkim_signing_attributes_origin,
16646 dkim_domain_signing_private_key: None,
16647 dkim_domain_signing_selector: None,
16648 dkim_next_signing_key_length: None,
16649 dkim_public_key_b64: None,
16650 email_forwarding_enabled,
16651 mail_from_domain,
16652 mail_from_behavior_on_mx_failure: mail_from_behavior,
16653 mail_from_domain_status: "Success".to_string(),
16654 configuration_set_name,
16655 };
16656 let mut accounts = self.ses_state.write();
16657 let state = accounts.get_or_create(&self.account_id);
16658 state.identities.insert(identity_name.clone(), identity);
16659 Ok(ProvisionResult::new(identity_name.clone()).with("EmailIdentity", identity_name))
16660 }
16661
16662 fn delete_ses_email_identity(&self, physical_id: &str) -> Result<(), String> {
16663 let mut accounts = self.ses_state.write();
16664 let state = accounts.get_or_create(&self.account_id);
16665 state.identities.remove(physical_id);
16666 Ok(())
16667 }
16668
16669 fn create_ses_template(
16670 &self,
16671 resource: &ResourceDefinition,
16672 ) -> Result<ProvisionResult, String> {
16673 let props = &resource.properties;
16674 let template_block = props
16675 .get("Template")
16676 .and_then(|v| v.as_object())
16677 .ok_or("Template is required")?;
16678 let template_name = template_block
16679 .get("TemplateName")
16680 .and_then(|v| v.as_str())
16681 .map(String::from)
16682 .unwrap_or_else(|| format!("cfn-tpl-{}", resource.logical_id));
16683 let tpl = SesEmailTemplate {
16684 template_name: template_name.clone(),
16685 subject: template_block
16686 .get("SubjectPart")
16687 .and_then(|v| v.as_str())
16688 .map(String::from),
16689 html_body: template_block
16690 .get("HtmlPart")
16691 .and_then(|v| v.as_str())
16692 .map(String::from),
16693 text_body: template_block
16694 .get("TextPart")
16695 .and_then(|v| v.as_str())
16696 .map(String::from),
16697 created_at: Utc::now(),
16698 };
16699 let mut accounts = self.ses_state.write();
16700 let state = accounts.get_or_create(&self.account_id);
16701 state.templates.insert(template_name.clone(), tpl);
16702 Ok(ProvisionResult::new(template_name.clone()).with("TemplateName", template_name))
16703 }
16704
16705 fn delete_ses_template(&self, physical_id: &str) -> Result<(), String> {
16706 let mut accounts = self.ses_state.write();
16707 let state = accounts.get_or_create(&self.account_id);
16708 state.templates.remove(physical_id);
16709 Ok(())
16710 }
16711
16712 fn create_ses_contact_list(
16713 &self,
16714 resource: &ResourceDefinition,
16715 ) -> Result<ProvisionResult, String> {
16716 let props = &resource.properties;
16717 let name = props
16718 .get("ContactListName")
16719 .and_then(|v| v.as_str())
16720 .map(String::from)
16721 .unwrap_or_else(|| format!("cfn-cl-{}", resource.logical_id));
16722 let description = props
16723 .get("Description")
16724 .and_then(|v| v.as_str())
16725 .map(String::from);
16726 let now = Utc::now();
16727 let cl = SesContactList {
16728 contact_list_name: name.clone(),
16729 description,
16730 topics: Vec::new(),
16731 created_at: now,
16732 last_updated_at: now,
16733 };
16734 let mut accounts = self.ses_state.write();
16735 let state = accounts.get_or_create(&self.account_id);
16736 state.contact_lists.insert(name.clone(), cl);
16737 Ok(ProvisionResult::new(name.clone()).with("ContactListName", name))
16738 }
16739
16740 fn delete_ses_contact_list(&self, physical_id: &str) -> Result<(), String> {
16741 let mut accounts = self.ses_state.write();
16742 let state = accounts.get_or_create(&self.account_id);
16743 state.contact_lists.remove(physical_id);
16744 state.contacts.remove(physical_id);
16745 Ok(())
16746 }
16747
16748 fn create_ses_dedicated_ip_pool(
16749 &self,
16750 resource: &ResourceDefinition,
16751 ) -> Result<ProvisionResult, String> {
16752 let props = &resource.properties;
16753 let name = props
16754 .get("PoolName")
16755 .and_then(|v| v.as_str())
16756 .map(String::from)
16757 .unwrap_or_else(|| format!("cfn-pool-{}", resource.logical_id));
16758 let scaling_mode = props
16759 .get("ScalingMode")
16760 .and_then(|v| v.as_str())
16761 .unwrap_or("STANDARD")
16762 .to_string();
16763 let pool = SesDedicatedIpPool {
16764 pool_name: name.clone(),
16765 scaling_mode,
16766 };
16767 let mut accounts = self.ses_state.write();
16768 let state = accounts.get_or_create(&self.account_id);
16769 state.dedicated_ip_pools.insert(name.clone(), pool);
16770 Ok(ProvisionResult::new(name.clone()).with("PoolName", name))
16771 }
16772
16773 fn delete_ses_dedicated_ip_pool(&self, physical_id: &str) -> Result<(), String> {
16774 let mut accounts = self.ses_state.write();
16775 let state = accounts.get_or_create(&self.account_id);
16776 state.dedicated_ip_pools.remove(physical_id);
16777 Ok(())
16778 }
16779
16780 fn create_ses_receipt_rule_set(
16781 &self,
16782 resource: &ResourceDefinition,
16783 ) -> Result<ProvisionResult, String> {
16784 let props = &resource.properties;
16785 let name = props
16786 .get("RuleSetName")
16787 .and_then(|v| v.as_str())
16788 .map(String::from)
16789 .unwrap_or_else(|| format!("cfn-rs-{}", resource.logical_id));
16790 let rs = SesReceiptRuleSet {
16791 name: name.clone(),
16792 rules: Vec::new(),
16793 created_at: Utc::now(),
16794 };
16795 let mut accounts = self.ses_state.write();
16796 let state = accounts.get_or_create(&self.account_id);
16797 state.receipt_rule_sets.insert(name.clone(), rs);
16798 Ok(ProvisionResult::new(name.clone()).with("RuleSetName", name))
16799 }
16800
16801 fn delete_ses_receipt_rule_set(&self, physical_id: &str) -> Result<(), String> {
16802 let mut accounts = self.ses_state.write();
16803 let state = accounts.get_or_create(&self.account_id);
16804 state.receipt_rule_sets.remove(physical_id);
16805 Ok(())
16806 }
16807
16808 fn create_ses_receipt_rule(
16809 &self,
16810 resource: &ResourceDefinition,
16811 ) -> Result<ProvisionResult, String> {
16812 let props = &resource.properties;
16813 let rule_set_name = props
16814 .get("RuleSetName")
16815 .and_then(|v| v.as_str())
16816 .ok_or("RuleSetName is required")?
16817 .to_string();
16818 let rule_block = props
16819 .get("Rule")
16820 .and_then(|v| v.as_object())
16821 .ok_or("Rule is required")?;
16822 let name = rule_block
16823 .get("Name")
16824 .and_then(|v| v.as_str())
16825 .map(String::from)
16826 .unwrap_or_else(|| format!("cfn-rule-{}", resource.logical_id));
16827 let enabled = rule_block
16828 .get("Enabled")
16829 .and_then(|v| v.as_bool())
16830 .unwrap_or(true);
16831 let scan_enabled = rule_block
16832 .get("ScanEnabled")
16833 .and_then(|v| v.as_bool())
16834 .unwrap_or(false);
16835 let tls_policy = rule_block
16836 .get("TlsPolicy")
16837 .and_then(|v| v.as_str())
16838 .unwrap_or("Optional")
16839 .to_string();
16840 let recipients: Vec<String> = rule_block
16841 .get("Recipients")
16842 .and_then(|v| v.as_array())
16843 .map(|arr| {
16844 arr.iter()
16845 .filter_map(|v| v.as_str().map(String::from))
16846 .collect()
16847 })
16848 .unwrap_or_default();
16849 let actions: Vec<SesReceiptAction> = rule_block
16850 .get("Actions")
16851 .and_then(|v| v.as_array())
16852 .map(|arr| arr.iter().filter_map(parse_ses_receipt_action).collect())
16853 .unwrap_or_default();
16854
16855 let rule = SesReceiptRule {
16856 name: name.clone(),
16857 enabled,
16858 scan_enabled,
16859 tls_policy,
16860 recipients,
16861 actions,
16862 };
16863 let mut accounts = self.ses_state.write();
16864 let state = accounts.get_or_create(&self.account_id);
16865 let rs = state
16866 .receipt_rule_sets
16867 .get_mut(&rule_set_name)
16868 .ok_or_else(|| format!("ReceiptRuleSet {rule_set_name} not yet provisioned"))?;
16869 rs.rules.retain(|r| r.name != name);
16870 rs.rules.push(rule);
16871 let physical = format!("{rule_set_name}|{name}");
16872 Ok(ProvisionResult::new(physical)
16873 .with("Name", name)
16874 .with("RuleSetName", rule_set_name))
16875 }
16876
16877 fn delete_ses_receipt_rule(
16878 &self,
16879 physical_id: &str,
16880 _attributes: &BTreeMap<String, String>,
16881 ) -> Result<(), String> {
16882 let mut parts = physical_id.splitn(2, '|');
16883 let Some(rs_name) = parts.next() else {
16884 return Ok(());
16885 };
16886 let Some(rule_name) = parts.next() else {
16887 return Ok(());
16888 };
16889 let mut accounts = self.ses_state.write();
16890 let state = accounts.get_or_create(&self.account_id);
16891 if let Some(rs) = state.receipt_rule_sets.get_mut(rs_name) {
16892 rs.rules.retain(|r| r.name != rule_name);
16893 }
16894 Ok(())
16895 }
16896
16897 fn create_ses_receipt_filter(
16898 &self,
16899 resource: &ResourceDefinition,
16900 ) -> Result<ProvisionResult, String> {
16901 let props = &resource.properties;
16902 let filter_block = props
16903 .get("Filter")
16904 .and_then(|v| v.as_object())
16905 .ok_or("Filter is required")?;
16906 let name = filter_block
16907 .get("Name")
16908 .and_then(|v| v.as_str())
16909 .map(String::from)
16910 .unwrap_or_else(|| format!("cfn-filter-{}", resource.logical_id));
16911 let ip_block = filter_block
16912 .get("IpFilter")
16913 .and_then(|v| v.as_object())
16914 .ok_or("Filter.IpFilter is required")?;
16915 let cidr = ip_block
16916 .get("Cidr")
16917 .and_then(|v| v.as_str())
16918 .ok_or("Filter.IpFilter.Cidr is required")?
16919 .to_string();
16920 let policy = ip_block
16921 .get("Policy")
16922 .and_then(|v| v.as_str())
16923 .unwrap_or("Block")
16924 .to_string();
16925 let filter = SesReceiptFilter {
16926 name: name.clone(),
16927 ip_filter: SesIpFilter { cidr, policy },
16928 };
16929 let mut accounts = self.ses_state.write();
16930 let state = accounts.get_or_create(&self.account_id);
16931 state.receipt_filters.insert(name.clone(), filter);
16932 Ok(ProvisionResult::new(name.clone()).with("Name", name))
16933 }
16934
16935 fn delete_ses_receipt_filter(&self, physical_id: &str) -> Result<(), String> {
16936 let mut accounts = self.ses_state.write();
16937 let state = accounts.get_or_create(&self.account_id);
16938 state.receipt_filters.remove(physical_id);
16939 Ok(())
16940 }
16941
16942 fn create_ses_vdm_attributes(
16943 &self,
16944 resource: &ResourceDefinition,
16945 ) -> Result<ProvisionResult, String> {
16946 let props = &resource.properties;
16947 let mut accounts = self.ses_state.write();
16948 let state = accounts.get_or_create(&self.account_id);
16949 state.account_settings.vdm_attributes = Some(props.clone());
16950 Ok(ProvisionResult::new(format!("vdm-{}", resource.logical_id)))
16951 }
16952
16953 fn create_secrets_manager_rotation_schedule(
16956 &self,
16957 resource: &ResourceDefinition,
16958 ) -> Result<ProvisionResult, String> {
16959 let props = &resource.properties;
16960 let secret_id = props
16961 .get("SecretId")
16962 .and_then(|v| v.as_str())
16963 .ok_or("SecretId is required")?
16964 .to_string();
16965 let rotation_lambda_arn = props
16966 .get("RotationLambdaARN")
16967 .and_then(|v| v.as_str())
16968 .map(String::from);
16969 let automatically_after_days = props
16970 .get("RotationRules")
16971 .and_then(|v| v.get("AutomaticallyAfterDays"))
16972 .and_then(|v| v.as_i64());
16973 let mut accounts = self.secretsmanager_state.write();
16974 let state = accounts.get_or_create(&self.account_id);
16975 let secret_arn = if state.secrets.contains_key(&secret_id) {
16976 secret_id.clone()
16977 } else {
16978 let candidate = format!(
16979 "arn:aws:secretsmanager:{}:{}:secret:{}",
16980 state.region, state.account_id, secret_id
16981 );
16982 if state.secrets.contains_key(&candidate) {
16983 candidate
16984 } else {
16985 return Err(format!("Secret {secret_id} not yet provisioned"));
16986 }
16987 };
16988 let secret = state
16989 .secrets
16990 .get_mut(&secret_arn)
16991 .ok_or_else(|| format!("Secret {secret_arn} not found"))?;
16992 secret.rotation_enabled = Some(true);
16993 secret.rotation_lambda_arn = rotation_lambda_arn;
16994 secret.rotation_rules = Some(RotationRules {
16995 automatically_after_days,
16996 });
16997 secret.last_changed_at = Utc::now();
16998 Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
16999 }
17000
17001 fn delete_secrets_manager_rotation_schedule(&self, physical_id: &str) -> Result<(), String> {
17002 let mut accounts = self.secretsmanager_state.write();
17003 let state = accounts.get_or_create(&self.account_id);
17004 if let Some(secret) = state.secrets.get_mut(physical_id) {
17005 secret.rotation_enabled = Some(false);
17006 secret.rotation_lambda_arn = None;
17007 secret.rotation_rules = None;
17008 secret.last_changed_at = Utc::now();
17009 }
17010 Ok(())
17011 }
17012
17013 fn create_secrets_manager_resource_policy(
17014 &self,
17015 resource: &ResourceDefinition,
17016 ) -> Result<ProvisionResult, String> {
17017 let props = &resource.properties;
17018 let secret_id = props
17019 .get("SecretId")
17020 .and_then(|v| v.as_str())
17021 .ok_or("SecretId is required")?
17022 .to_string();
17023 let policy_doc = props
17024 .get("ResourcePolicy")
17025 .ok_or("ResourcePolicy is required")?;
17026 let policy_str = match policy_doc {
17027 serde_json::Value::String(s) => s.clone(),
17028 other => other.to_string(),
17029 };
17030 let mut accounts = self.secretsmanager_state.write();
17031 let state = accounts.get_or_create(&self.account_id);
17032 let secret_arn = if state.secrets.contains_key(&secret_id) {
17033 secret_id.clone()
17034 } else {
17035 let candidate = format!(
17036 "arn:aws:secretsmanager:{}:{}:secret:{}",
17037 state.region, state.account_id, secret_id
17038 );
17039 if state.secrets.contains_key(&candidate) {
17040 candidate
17041 } else {
17042 return Err(format!("Secret {secret_id} not yet provisioned"));
17043 }
17044 };
17045 let secret = state
17046 .secrets
17047 .get_mut(&secret_arn)
17048 .ok_or_else(|| format!("Secret {secret_arn} not found"))?;
17049 secret.resource_policy = Some(policy_str);
17050 secret.last_changed_at = Utc::now();
17051 Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
17052 }
17053
17054 fn delete_secrets_manager_resource_policy(&self, physical_id: &str) -> Result<(), String> {
17055 let mut accounts = self.secretsmanager_state.write();
17056 let state = accounts.get_or_create(&self.account_id);
17057 if let Some(secret) = state.secrets.get_mut(physical_id) {
17058 secret.resource_policy = None;
17059 secret.last_changed_at = Utc::now();
17060 }
17061 Ok(())
17062 }
17063
17064 fn create_secrets_manager_target_attachment(
17065 &self,
17066 resource: &ResourceDefinition,
17067 ) -> Result<ProvisionResult, String> {
17068 let props = &resource.properties;
17069 let secret_id = props
17070 .get("SecretId")
17071 .and_then(|v| v.as_str())
17072 .ok_or("SecretId is required")?
17073 .to_string();
17074 let target_type = props
17075 .get("TargetType")
17076 .and_then(|v| v.as_str())
17077 .ok_or("TargetType is required")?;
17078 let target_id = props
17079 .get("TargetId")
17080 .and_then(|v| v.as_str())
17081 .ok_or("TargetId is required")?;
17082 let mut accounts = self.secretsmanager_state.write();
17083 let state = accounts.get_or_create(&self.account_id);
17084 let secret_arn = if state.secrets.contains_key(&secret_id) {
17085 secret_id.clone()
17086 } else {
17087 let candidate = format!(
17088 "arn:aws:secretsmanager:{}:{}:secret:{}",
17089 state.region, state.account_id, secret_id
17090 );
17091 if state.secrets.contains_key(&candidate) {
17092 candidate
17093 } else {
17094 return Err(format!("Secret {secret_id} not yet provisioned"));
17095 }
17096 };
17097 let secret = state
17098 .secrets
17099 .get_mut(&secret_arn)
17100 .ok_or_else(|| format!("Secret {secret_arn} not found"))?;
17101 let now = Utc::now();
17107 if secret.current_version_id.is_none() {
17108 let version_id = Uuid::new_v4().to_string();
17109 secret.versions.insert(
17110 version_id.clone(),
17111 SecretVersion {
17112 version_id: version_id.clone(),
17113 secret_string: Some("{}".to_string()),
17114 secret_binary: None,
17115 stages: vec!["AWSCURRENT".to_string()],
17116 created_at: now,
17117 },
17118 );
17119 secret.current_version_id = Some(version_id);
17120 }
17121 if let Some(version_id) = secret.current_version_id.clone() {
17122 if let Some(version) = secret.versions.get_mut(&version_id) {
17123 let mut existing: serde_json::Value = version
17124 .secret_string
17125 .as_deref()
17126 .and_then(|s| serde_json::from_str(s).ok())
17127 .unwrap_or_else(|| serde_json::json!({}));
17128 if let Some(obj) = existing.as_object_mut() {
17129 let engine = match target_type {
17130 "AWS::RDS::DBInstance" | "AWS::RDS::DBCluster" => "postgres",
17131 _ => "unknown",
17132 };
17133 obj.entry("engine".to_string())
17134 .or_insert(serde_json::json!(engine));
17135 obj.insert("host".to_string(), serde_json::json!(target_id));
17136 obj.entry("dbInstanceIdentifier".to_string())
17137 .or_insert(serde_json::json!(target_id));
17138 }
17139 version.secret_string = Some(existing.to_string());
17140 }
17141 }
17142 secret.last_changed_at = now;
17143 Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
17144 }
17145
17146 fn create_athena_work_group(
17149 &self,
17150 resource: &ResourceDefinition,
17151 ) -> Result<ProvisionResult, String> {
17152 let props = &resource.properties;
17153 let name = props
17154 .get("Name")
17155 .and_then(|v| v.as_str())
17156 .unwrap_or(&resource.logical_id)
17157 .to_string();
17158 let description = props
17159 .get("Description")
17160 .and_then(|v| v.as_str())
17161 .map(|s| s.to_string());
17162 let configuration = props.get("Configuration").cloned();
17163 let state_str = props
17164 .get("State")
17165 .and_then(|v| v.as_str())
17166 .unwrap_or("ENABLED");
17167 let tags = Self::parse_athena_tags(props.get("Tags"));
17168
17169 let mut accounts = self.athena_state.write();
17170 let account = accounts
17171 .accounts
17172 .entry(self.account_id.clone())
17173 .or_default();
17174 account.ensure_initialized();
17175 if account.work_groups.contains_key(&name) {
17176 return Err(format!("WorkGroup {name} already exists"));
17177 }
17178 let wg = WorkGroup {
17179 name: name.clone(),
17180 state: state_str.to_string(),
17181 description,
17182 configuration,
17183 creation_time: Utc::now(),
17184 engine_version: Some("AUTO".to_string()),
17185 };
17186 let arn = format!(
17187 "arn:aws:athena:{}:{}:workgroup/{}",
17188 self.region, self.account_id, name
17189 );
17190 account.work_groups.insert(name.clone(), wg);
17191 if !tags.is_empty() {
17192 account.tags.insert(arn.clone(), tags);
17193 }
17194 Ok(ProvisionResult::new(name.clone())
17195 .with("Arn", arn)
17196 .with("Name", name))
17197 }
17198
17199 fn delete_athena_work_group(&self, physical_id: &str) -> Result<(), String> {
17200 let mut accounts = self.athena_state.write();
17201 let account = accounts
17202 .accounts
17203 .entry(self.account_id.clone())
17204 .or_default();
17205 account.work_groups.remove(physical_id);
17206 Ok(())
17207 }
17208
17209 fn get_att_athena_work_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
17210 let mut accounts = self.athena_state.write();
17211 let account = accounts
17212 .accounts
17213 .entry(self.account_id.clone())
17214 .or_default();
17215 let wg = account.work_groups.get(physical_id)?;
17216 match attribute {
17217 "Arn" => Some(format!(
17218 "arn:aws:athena:{}:{}:workgroup/{}",
17219 self.region, self.account_id, wg.name
17220 )),
17221 "Name" => Some(wg.name.clone()),
17222 _ => None,
17223 }
17224 }
17225
17226 fn create_athena_data_catalog(
17227 &self,
17228 resource: &ResourceDefinition,
17229 ) -> Result<ProvisionResult, String> {
17230 let props = &resource.properties;
17231 let name = props
17232 .get("Name")
17233 .and_then(|v| v.as_str())
17234 .unwrap_or(&resource.logical_id)
17235 .to_string();
17236 let cat_type = props
17237 .get("Type")
17238 .and_then(|v| v.as_str())
17239 .ok_or("Type is required")?
17240 .to_string();
17241 let description = props
17242 .get("Description")
17243 .and_then(|v| v.as_str())
17244 .map(|s| s.to_string());
17245 let parameters = props
17246 .get("Parameters")
17247 .and_then(|v| v.as_object())
17248 .map(|m| {
17249 m.iter()
17250 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
17251 .collect()
17252 })
17253 .unwrap_or_default();
17254 let connection_type = props
17255 .get("ConnectionType")
17256 .and_then(|v| v.as_str())
17257 .map(|s| s.to_string());
17258 let tags = Self::parse_athena_tags(props.get("Tags"));
17259
17260 let mut accounts = self.athena_state.write();
17261 let account = accounts
17262 .accounts
17263 .entry(self.account_id.clone())
17264 .or_default();
17265 account.ensure_initialized();
17266 if account.data_catalogs.contains_key(&name) {
17267 return Err(format!("DataCatalog {name} already exists"));
17268 }
17269 let cat = DataCatalog {
17270 name: name.clone(),
17271 description,
17272 cat_type,
17273 parameters,
17274 status: "CREATE_COMPLETE".to_string(),
17275 connection_type,
17276 error: None,
17277 };
17278 let arn = format!(
17279 "arn:aws:athena:{}:{}:datacatalog/{}",
17280 self.region, self.account_id, name
17281 );
17282 account.data_catalogs.insert(name.clone(), cat);
17283 if !tags.is_empty() {
17284 account.tags.insert(arn.clone(), tags);
17285 }
17286 Ok(ProvisionResult::new(name.clone())
17287 .with("Arn", arn)
17288 .with("Name", name))
17289 }
17290
17291 fn delete_athena_data_catalog(&self, physical_id: &str) -> Result<(), String> {
17292 let mut accounts = self.athena_state.write();
17293 let account = accounts
17294 .accounts
17295 .entry(self.account_id.clone())
17296 .or_default();
17297 account.data_catalogs.remove(physical_id);
17298 Ok(())
17299 }
17300
17301 fn get_att_athena_data_catalog(&self, physical_id: &str, attribute: &str) -> Option<String> {
17302 let mut accounts = self.athena_state.write();
17303 let account = accounts
17304 .accounts
17305 .entry(self.account_id.clone())
17306 .or_default();
17307 let cat = account.data_catalogs.get(physical_id)?;
17308 match attribute {
17309 "Arn" => Some(format!(
17310 "arn:aws:athena:{}:{}:datacatalog/{}",
17311 self.region, self.account_id, cat.name
17312 )),
17313 "Name" => Some(cat.name.clone()),
17314 _ => None,
17315 }
17316 }
17317
17318 fn create_athena_named_query(
17319 &self,
17320 resource: &ResourceDefinition,
17321 ) -> Result<ProvisionResult, String> {
17322 let props = &resource.properties;
17323 let name = props
17324 .get("Name")
17325 .and_then(|v| v.as_str())
17326 .ok_or("Name is required")?
17327 .to_string();
17328 let database = props
17329 .get("Database")
17330 .and_then(|v| v.as_str())
17331 .ok_or("Database is required")?
17332 .to_string();
17333 let query_string = props
17334 .get("QueryString")
17335 .and_then(|v| v.as_str())
17336 .ok_or("QueryString is required")?
17337 .to_string();
17338 let description = props
17339 .get("Description")
17340 .and_then(|v| v.as_str())
17341 .map(|s| s.to_string());
17342 let work_group = props
17343 .get("WorkGroup")
17344 .and_then(|v| v.as_str())
17345 .unwrap_or("primary")
17346 .to_string();
17347
17348 let mut accounts = self.athena_state.write();
17349 let account = accounts
17350 .accounts
17351 .entry(self.account_id.clone())
17352 .or_default();
17353 account.ensure_initialized();
17354 if !account.work_groups.contains_key(&work_group) {
17355 return Err(format!("Workgroup {work_group} not found"));
17356 }
17357 let id = Uuid::new_v4().to_string();
17358 let nq = NamedQuery {
17359 named_query_id: id.clone(),
17360 name,
17361 description,
17362 database,
17363 query_string,
17364 work_group,
17365 last_used_at: None,
17366 };
17367 account.named_queries.insert(id.clone(), nq);
17368 Ok(ProvisionResult::new(id.clone()).with("NamedQueryId", id))
17369 }
17370
17371 fn delete_athena_named_query(&self, physical_id: &str) -> Result<(), String> {
17372 let mut accounts = self.athena_state.write();
17373 let account = accounts
17374 .accounts
17375 .entry(self.account_id.clone())
17376 .or_default();
17377 account.named_queries.remove(physical_id);
17378 Ok(())
17379 }
17380
17381 fn get_att_athena_named_query(&self, physical_id: &str, attribute: &str) -> Option<String> {
17382 let mut accounts = self.athena_state.write();
17383 let account = accounts
17384 .accounts
17385 .entry(self.account_id.clone())
17386 .or_default();
17387 let nq = account.named_queries.get(physical_id)?;
17388 match attribute {
17389 "NamedQueryId" => Some(nq.named_query_id.clone()),
17390 _ => None,
17391 }
17392 }
17393
17394 fn create_athena_prepared_statement(
17395 &self,
17396 resource: &ResourceDefinition,
17397 ) -> Result<ProvisionResult, String> {
17398 let props = &resource.properties;
17399 let statement_name = props
17400 .get("StatementName")
17401 .and_then(|v| v.as_str())
17402 .ok_or("StatementName is required")?
17403 .to_string();
17404 let work_group_name = props
17405 .get("WorkGroupName")
17406 .and_then(|v| v.as_str())
17407 .ok_or("WorkGroupName is required")?
17408 .to_string();
17409 let query_statement = props
17410 .get("QueryStatement")
17411 .and_then(|v| v.as_str())
17412 .ok_or("QueryStatement is required")?
17413 .to_string();
17414 let description = props
17415 .get("Description")
17416 .and_then(|v| v.as_str())
17417 .map(|s| s.to_string());
17418
17419 let mut accounts = self.athena_state.write();
17420 let account = accounts
17421 .accounts
17422 .entry(self.account_id.clone())
17423 .or_default();
17424 account.ensure_initialized();
17425 if !account.work_groups.contains_key(&work_group_name) {
17426 return Err(format!("Workgroup {work_group_name} not found"));
17427 }
17428 let key = (work_group_name.clone(), statement_name.clone());
17429 if account.prepared_statements.contains_key(&key) {
17430 return Err(format!(
17431 "PreparedStatement {statement_name} already exists in {work_group_name}"
17432 ));
17433 }
17434 let ps = PreparedStatement {
17435 statement_name: statement_name.clone(),
17436 work_group_name: work_group_name.clone(),
17437 query_statement,
17438 description,
17439 last_modified_time: Utc::now(),
17440 };
17441 let physical_id = format!("{work_group_name}|{statement_name}");
17442 account.prepared_statements.insert(key, ps);
17443 Ok(ProvisionResult::new(physical_id))
17444 }
17445
17446 fn delete_athena_prepared_statement(
17447 &self,
17448 physical_id: &str,
17449 _attrs: &BTreeMap<String, String>,
17450 ) -> Result<(), String> {
17451 let mut accounts = self.athena_state.write();
17452 let account = accounts
17453 .accounts
17454 .entry(self.account_id.clone())
17455 .or_default();
17456 let parts: Vec<&str> = physical_id.split('|').collect();
17457 if parts.len() != 2 {
17458 return Err(format!(
17459 "Invalid PreparedStatement physical id: {physical_id}"
17460 ));
17461 }
17462 let key = (parts[0].to_string(), parts[1].to_string());
17463 account.prepared_statements.remove(&key);
17464 Ok(())
17465 }
17466
17467 fn get_att_athena_prepared_statement(
17468 &self,
17469 physical_id: &str,
17470 attribute: &str,
17471 ) -> Option<String> {
17472 let mut accounts = self.athena_state.write();
17473 let account = accounts
17474 .accounts
17475 .entry(self.account_id.clone())
17476 .or_default();
17477 let parts: Vec<&str> = physical_id.split('|').collect();
17478 if parts.len() != 2 {
17479 return None;
17480 }
17481 let ps = account
17482 .prepared_statements
17483 .get(&(parts[0].to_string(), parts[1].to_string()))?;
17484 match attribute {
17485 "StatementName" => Some(ps.statement_name.clone()),
17486 "WorkGroupName" => Some(ps.work_group_name.clone()),
17487 _ => None,
17488 }
17489 }
17490
17491 fn parse_athena_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
17492 let mut out = BTreeMap::new();
17493 let Some(arr) = value.and_then(|v| v.as_array()) else {
17494 return out;
17495 };
17496 for tag in arr {
17497 if let (Some(k), Some(v)) = (
17498 tag.get("Key").and_then(|v| v.as_str()),
17499 tag.get("Value").and_then(|v| v.as_str()),
17500 ) {
17501 out.insert(k.to_string(), v.to_string());
17502 }
17503 }
17504 out
17505 }
17506
17507 fn create_glue_database(
17508 &self,
17509 resource: &ResourceDefinition,
17510 ) -> Result<ProvisionResult, String> {
17511 let props = &resource.properties;
17512 let input = props
17513 .get("DatabaseInput")
17514 .ok_or("DatabaseInput is required")?;
17515 let name = input
17516 .get("Name")
17517 .and_then(|v| v.as_str())
17518 .unwrap_or(&resource.logical_id)
17519 .to_string();
17520 let description = input
17521 .get("Description")
17522 .and_then(|v| v.as_str())
17523 .map(|s| s.to_string());
17524 let location_uri = input
17525 .get("LocationUri")
17526 .and_then(|v| v.as_str())
17527 .map(|s| s.to_string());
17528 let parameters = input
17529 .get("Parameters")
17530 .and_then(|v| v.as_object())
17531 .map(|m| {
17532 m.iter()
17533 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
17534 .collect()
17535 })
17536 .unwrap_or_default();
17537
17538 let mut accounts = self.glue_state.write();
17539 let state = accounts.get_or_create(&self.account_id, &self.region);
17540 let dbs = state.dbs_in_mut(&self.region);
17541 if dbs.contains_key(&name) {
17542 return Err(format!("Database {name} already exists"));
17543 }
17544 dbs.insert(
17545 name.clone(),
17546 fakecloud_glue::Database {
17547 name: name.clone(),
17548 description,
17549 location_uri,
17550 parameters,
17551 created_at: Utc::now(),
17552 catalog_id: self.account_id.clone(),
17553 tables: BTreeMap::new(),
17554 },
17555 );
17556 Ok(ProvisionResult::new(name.clone()))
17557 }
17558
17559 fn delete_glue_database(&self, physical_id: &str) -> Result<(), String> {
17560 let mut accounts = self.glue_state.write();
17561 let state = accounts.get_or_create(&self.account_id, &self.region);
17562 state.dbs_in_mut(&self.region).remove(physical_id);
17563 Ok(())
17564 }
17565
17566 fn create_glue_table(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
17567 let props = &resource.properties;
17568 let db_name = props
17569 .get("DatabaseName")
17570 .and_then(|v| v.as_str())
17571 .ok_or("DatabaseName is required")?
17572 .to_string();
17573 let input = props.get("TableInput").ok_or("TableInput is required")?;
17574 let name = input
17575 .get("Name")
17576 .and_then(|v| v.as_str())
17577 .ok_or("TableInput.Name is required")?
17578 .to_string();
17579 let now = Utc::now();
17580
17581 let mut accounts = self.glue_state.write();
17582 let state = accounts.get_or_create(&self.account_id, &self.region);
17583 let dbs = state.dbs_in_mut(&self.region);
17584 let db = dbs
17585 .get_mut(&db_name)
17586 .ok_or_else(|| format!("Database {db_name} not found"))?;
17587 if db.tables.contains_key(&name) {
17588 return Err(format!("Table {name} already exists"));
17589 }
17590 db.tables.insert(
17591 name.clone(),
17592 fakecloud_glue::Table {
17593 name: name.clone(),
17594 database_name: db_name.clone(),
17595 description: input
17596 .get("Description")
17597 .and_then(|v| v.as_str())
17598 .map(|s| s.to_string()),
17599 owner: input
17600 .get("Owner")
17601 .and_then(|v| v.as_str())
17602 .map(|s| s.to_string()),
17603 create_time: now,
17604 update_time: now,
17605 last_access_time: None,
17606 retention: input.get("Retention").and_then(|v| v.as_i64()).unwrap_or(0),
17607 storage_descriptor: None,
17608 partition_keys: Vec::new(),
17609 view_original_text: input
17610 .get("ViewOriginalText")
17611 .and_then(|v| v.as_str())
17612 .map(|s| s.to_string()),
17613 view_expanded_text: input
17614 .get("ViewExpandedText")
17615 .and_then(|v| v.as_str())
17616 .map(|s| s.to_string()),
17617 table_type: input
17618 .get("TableType")
17619 .and_then(|v| v.as_str())
17620 .map(|s| s.to_string()),
17621 parameters: BTreeMap::new(),
17622 partitions: BTreeMap::new(),
17623 },
17624 );
17625 let physical_id = format!("{db_name}|{name}");
17626 Ok(ProvisionResult::new(physical_id))
17627 }
17628
17629 fn delete_glue_table(&self, physical_id: &str) -> Result<(), String> {
17630 let mut accounts = self.glue_state.write();
17631 let state = accounts.get_or_create(&self.account_id, &self.region);
17632 let parts: Vec<&str> = physical_id.split('|').collect();
17633 if parts.len() != 2 {
17634 return Err(format!("Invalid Glue table physical id: {physical_id}"));
17635 }
17636 let dbs = state.dbs_in_mut(&self.region);
17637 let db = dbs
17638 .get_mut(parts[0])
17639 .ok_or_else(|| format!("Database {} not found", parts[0]))?;
17640 db.tables.remove(parts[1]);
17641 Ok(())
17642 }
17643
17644 fn create_glue_partition(
17645 &self,
17646 resource: &ResourceDefinition,
17647 ) -> Result<ProvisionResult, String> {
17648 let props = &resource.properties;
17649 let db_name = props
17650 .get("DatabaseName")
17651 .and_then(|v| v.as_str())
17652 .ok_or("DatabaseName is required")?
17653 .to_string();
17654 let table_name = props
17655 .get("TableName")
17656 .and_then(|v| v.as_str())
17657 .ok_or("TableName is required")?
17658 .to_string();
17659 let input = props
17660 .get("PartitionInput")
17661 .ok_or("PartitionInput is required")?;
17662 let values: Vec<String> = input
17663 .get("Values")
17664 .and_then(|v| v.as_array())
17665 .map(|arr| {
17666 arr.iter()
17667 .filter_map(|v| v.as_str().map(|s| s.to_string()))
17668 .collect()
17669 })
17670 .unwrap_or_default();
17671 if values.is_empty() {
17672 return Err("PartitionInput.Values is required".to_string());
17673 }
17674 let key = values
17675 .iter()
17676 .map(|v| {
17677 v.replace('%', "%25")
17678 .replace('|', "%7C")
17679 .replace('/', "%2F")
17680 })
17681 .collect::<Vec<_>>()
17682 .join("/");
17683
17684 let mut accounts = self.glue_state.write();
17685 let state = accounts.get_or_create(&self.account_id, &self.region);
17686 let dbs = state.dbs_in_mut(&self.region);
17687 let db = dbs
17688 .get_mut(&db_name)
17689 .ok_or_else(|| format!("Database {db_name} not found"))?;
17690 let table = db
17691 .tables
17692 .get_mut(&table_name)
17693 .ok_or_else(|| format!("Table {table_name} not found"))?;
17694 if table.partitions.contains_key(&key) {
17695 return Err(format!("Partition {key} already exists"));
17696 }
17697 table.partitions.insert(
17698 key.clone(),
17699 fakecloud_glue::Partition {
17700 values: values.clone(),
17701 database_name: db_name.clone(),
17702 table_name: table_name.clone(),
17703 create_time: Utc::now(),
17704 last_access_time: None,
17705 storage_descriptor: None,
17706 parameters: BTreeMap::new(),
17707 },
17708 );
17709 let physical_id = format!("{db_name}|{table_name}|{key}");
17710 Ok(ProvisionResult::new(physical_id))
17711 }
17712
17713 fn delete_glue_partition(
17714 &self,
17715 physical_id: &str,
17716 _attrs: &BTreeMap<String, String>,
17717 ) -> Result<(), String> {
17718 let mut accounts = self.glue_state.write();
17719 let state = accounts.get_or_create(&self.account_id, &self.region);
17720 let parts: Vec<&str> = physical_id.split('|').collect();
17721 if parts.len() != 3 {
17722 return Err(format!("Invalid Glue partition physical id: {physical_id}"));
17723 }
17724 let dbs = state.dbs_in_mut(&self.region);
17725 let db = dbs
17726 .get_mut(parts[0])
17727 .ok_or_else(|| format!("Database {} not found", parts[0]))?;
17728 let table = db
17729 .tables
17730 .get_mut(parts[1])
17731 .ok_or_else(|| format!("Table {} not found", parts[1]))?;
17732 table.partitions.remove(parts[2]);
17733 Ok(())
17734 }
17735
17736 fn create_cloudformation_stack(
17739 &self,
17740 resource: &ResourceDefinition,
17741 ) -> Result<ProvisionResult, String> {
17742 let props = &resource.properties;
17743
17744 let template_body = if let Some(body) = props.get("TemplateBody").and_then(|v| v.as_str()) {
17745 body.to_string()
17746 } else if let Some(url) = props.get("TemplateURL").and_then(|v| v.as_str()) {
17747 self.fetch_template_from_url(url)?
17748 } else {
17749 return Err(
17750 "AWS::CloudFormation::Stack requires TemplateURL or TemplateBody".to_string(),
17751 );
17752 };
17753
17754 let child_parameters =
17755 if let Some(params) = props.get("Parameters").and_then(|v| v.as_object()) {
17756 let mut map = BTreeMap::new();
17757 for (k, v) in params {
17758 if let Some(s) = v.as_str() {
17759 map.insert(k.clone(), s.to_string());
17760 } else {
17761 map.insert(k.clone(), v.to_string());
17762 }
17763 }
17764 map
17765 } else {
17766 BTreeMap::new()
17767 };
17768
17769 let child_tags = if let Some(tags) = props.get("Tags").and_then(|v| v.as_object()) {
17770 let mut map = BTreeMap::new();
17771 for (k, v) in tags {
17772 if let Some(s) = v.as_str() {
17773 map.insert(k.clone(), s.to_string());
17774 }
17775 }
17776 map
17777 } else {
17778 BTreeMap::new()
17779 };
17780
17781 let parsed = crate::template::parse_template(&template_body, &child_parameters)
17782 .map_err(|e| format!("Failed to parse nested template: {e}"))?;
17783
17784 let child_stack_name = format!(
17785 "{}-Nested-{}",
17786 resource.logical_id,
17787 std::time::SystemTime::now()
17788 .duration_since(std::time::UNIX_EPOCH)
17789 .map(|d| d.as_nanos())
17790 .unwrap_or(0)
17791 );
17792 let child_stack_id = format!(
17793 "arn:aws:cloudformation:{}:{}:stack/{}/{}",
17794 self.region,
17795 self.account_id,
17796 child_stack_name,
17797 Uuid::new_v4()
17798 );
17799
17800 let child_provisioner = ResourceProvisioner {
17801 sqs_state: self.sqs_state.clone(),
17802 sns_state: self.sns_state.clone(),
17803 ssm_state: self.ssm_state.clone(),
17804 iam_state: self.iam_state.clone(),
17805 s3_state: self.s3_state.clone(),
17806 eventbridge_state: self.eventbridge_state.clone(),
17807 dynamodb_state: self.dynamodb_state.clone(),
17808 logs_state: self.logs_state.clone(),
17809 lambda_state: self.lambda_state.clone(),
17810 secretsmanager_state: self.secretsmanager_state.clone(),
17811 kinesis_state: self.kinesis_state.clone(),
17812 kms_state: self.kms_state.clone(),
17813 ecr_state: self.ecr_state.clone(),
17814 cloudwatch_state: self.cloudwatch_state.clone(),
17815 elbv2_state: self.elbv2_state.clone(),
17816 organizations_state: self.organizations_state.clone(),
17817 cognito_state: self.cognito_state.clone(),
17818 rds_state: self.rds_state.clone(),
17819 ecs_state: self.ecs_state.clone(),
17820 acm_state: self.acm_state.clone(),
17821 elasticache_state: self.elasticache_state.clone(),
17822 route53_state: self.route53_state.clone(),
17823 cloudfront_state: self.cloudfront_state.clone(),
17824 stepfunctions_state: self.stepfunctions_state.clone(),
17825 wafv2_state: self.wafv2_state.clone(),
17826 apigateway_state: self.apigateway_state.clone(),
17827 apigatewayv2_state: self.apigatewayv2_state.clone(),
17828 ses_state: self.ses_state.clone(),
17829 app_autoscaling_state: self.app_autoscaling_state.clone(),
17830 athena_state: self.athena_state.clone(),
17831 firehose_state: self.firehose_state.clone(),
17832 glue_state: self.glue_state.clone(),
17833 cloudformation_state: self.cloudformation_state.clone(),
17834 delivery: self.delivery.clone(),
17835 account_id: self.account_id.clone(),
17836 region: self.region.clone(),
17837 stack_id: child_stack_id.clone(),
17838 };
17839
17840 let child_resources = crate::service::provision_stack_resources(
17841 &child_provisioner,
17842 &parsed.resources,
17843 &template_body,
17844 &child_parameters,
17845 )
17846 .map_err(|e| format!("Failed to provision nested stack: {e}"))?;
17847
17848 let child_outputs = parsed.outputs;
17849
17850 let stack = crate::state::Stack {
17851 name: child_stack_name.clone(),
17852 stack_id: child_stack_id.clone(),
17853 template: template_body.clone(),
17854 status: "CREATE_COMPLETE".to_string(),
17855 resources: child_resources.clone(),
17856 parameters: child_parameters,
17857 tags: child_tags,
17858 created_at: Utc::now(),
17859 updated_at: None,
17860 description: parsed.description,
17861 notification_arns: Vec::new(),
17862 outputs: child_outputs
17863 .iter()
17864 .map(|o| crate::state::StackOutput {
17865 key: o.logical_id.clone(),
17866 value: o.value.clone(),
17867 description: o.description.clone(),
17868 export_name: o.export_name.clone(),
17869 })
17870 .collect(),
17871 };
17872
17873 {
17874 let mut accounts = self.cloudformation_state.write();
17875 let state = accounts.get_or_create(&self.account_id);
17876 state.stacks.insert(child_stack_name.clone(), stack);
17877
17878 crate::service::record_stack_status_event(
17879 state,
17880 &child_stack_id,
17881 &child_stack_name,
17882 "AWS::CloudFormation::Stack",
17883 "CREATE_IN_PROGRESS",
17884 );
17885 let changes: Vec<crate::service::ResourceChange> = child_resources
17886 .iter()
17887 .map(|r| crate::service::ResourceChange {
17888 action: crate::service::ResourceChangeAction::Create,
17889 logical_id: r.logical_id.clone(),
17890 physical_id: r.physical_id.clone(),
17891 resource_type: r.resource_type.clone(),
17892 })
17893 .collect();
17894 crate::service::record_stack_events(
17895 state,
17896 &child_stack_id,
17897 &child_stack_name,
17898 &changes,
17899 );
17900 crate::service::record_stack_status_event(
17901 state,
17902 &child_stack_id,
17903 &child_stack_name,
17904 "AWS::CloudFormation::Stack",
17905 "CREATE_COMPLETE",
17906 );
17907 }
17908
17909 let mut result = ProvisionResult::new(child_stack_id.clone());
17910 for output in &child_outputs {
17911 result.attributes.insert(
17912 format!("Outputs.{}", output.logical_id),
17913 output.value.clone(),
17914 );
17915 }
17916 Ok(result)
17917 }
17918
17919 fn delete_cloudformation_stack(&self, physical_id: &str) -> Result<(), String> {
17920 let stack = {
17921 let accounts = self.cloudformation_state.read();
17922 let state = accounts.get(&self.account_id);
17923 state.and_then(|s| {
17924 s.stacks
17925 .values()
17926 .find(|st| st.stack_id == physical_id)
17927 .cloned()
17928 })
17929 };
17930
17931 if let Some(stack) = stack {
17932 let stack_name = stack.name.clone();
17933 let stack_id = stack.stack_id.clone();
17934
17935 for resource in stack.resources.iter().rev() {
17936 let _ = self.delete_resource(resource);
17937 }
17938
17939 {
17940 let mut accounts = self.cloudformation_state.write();
17941 let state = accounts.get_or_create(&self.account_id);
17942 state.stacks.remove(&stack_name);
17943
17944 crate::service::record_stack_status_event(
17945 state,
17946 &stack_id,
17947 &stack_name,
17948 "AWS::CloudFormation::Stack",
17949 "DELETE_IN_PROGRESS",
17950 );
17951 crate::service::record_stack_status_event(
17952 state,
17953 &stack_id,
17954 &stack_name,
17955 "AWS::CloudFormation::Stack",
17956 "DELETE_COMPLETE",
17957 );
17958 }
17959 }
17960
17961 Ok(())
17962 }
17963
17964 fn get_att_cloudformation_stack(&self, physical_id: &str, attribute: &str) -> Option<String> {
17965 let accounts = self.cloudformation_state.read();
17966 let state = accounts.get(&self.account_id)?;
17967 let stack = state.stacks.values().find(|s| s.stack_id == physical_id)?;
17968
17969 if let Some(output_key) = attribute.strip_prefix("Outputs.") {
17970 return stack
17971 .outputs
17972 .iter()
17973 .find(|o| o.key == output_key)
17974 .map(|o| o.value.clone());
17975 }
17976
17977 match attribute {
17978 "Outputs" => Some(
17979 serde_json::to_string(
17980 &stack
17981 .outputs
17982 .iter()
17983 .map(|o| (o.key.clone(), o.value.clone()))
17984 .collect::<std::collections::BTreeMap<String, String>>(),
17985 )
17986 .unwrap_or_default(),
17987 ),
17988 _ => None,
17989 }
17990 }
17991
17992 fn fetch_template_from_url(&self, url: &str) -> Result<String, String> {
17993 if let Some(rest) = url.strip_prefix("s3://") {
17994 let parts: Vec<&str> = rest.splitn(2, '/').collect();
17995 if parts.len() != 2 {
17996 return Err("Invalid s3:// URL".to_string());
17997 }
17998 return self.fetch_s3_template(parts[0], parts[1]);
17999 }
18000
18001 if let Some(rest) = url.strip_prefix("https://s3.amazonaws.com/") {
18002 let parts: Vec<&str> = rest.splitn(2, '/').collect();
18003 if parts.len() != 2 {
18004 return Err("Invalid S3 HTTPS URL".to_string());
18005 }
18006 return self.fetch_s3_template(parts[0], parts[1]);
18007 }
18008
18009 if let Some(host_rest) = url.strip_prefix("https://") {
18010 if let Some(slash_pos) = host_rest.find('/') {
18011 let host = &host_rest[..slash_pos];
18012 let key = &host_rest[slash_pos + 1..];
18013 if let Some(bucket) = host.strip_suffix(".s3.amazonaws.com") {
18014 return self.fetch_s3_template(bucket, key);
18015 }
18016 if host.contains(".s3.") && host.ends_with(".amazonaws.com") {
18017 let bucket = host.split(".s3.").next().unwrap_or("");
18018 if !bucket.is_empty() {
18019 return self.fetch_s3_template(bucket, key);
18020 }
18021 }
18022 }
18023 }
18024
18025 Err(format!("Unsupported TemplateURL: {url}"))
18026 }
18027
18028 fn fetch_s3_template(&self, bucket: &str, key: &str) -> Result<String, String> {
18029 let mut s3_accounts = self.s3_state.write();
18030 let s3_state = s3_accounts.get_or_create(&self.account_id);
18031 let bucket_obj = s3_state
18032 .buckets
18033 .get(bucket)
18034 .ok_or_else(|| format!("S3 bucket not found: {bucket}"))?;
18035 let obj = bucket_obj
18036 .objects
18037 .get(key)
18038 .ok_or_else(|| format!("S3 object not found: {bucket}/{key}"))?;
18039 let bytes = s3_state
18040 .read_body(&obj.body)
18041 .map_err(|e| format!("Failed to read S3 object body: {e}"))?;
18042 String::from_utf8(bytes.to_vec()).map_err(|e| format!("S3 object is not valid UTF-8: {e}"))
18043 }
18044}
18045
18046fn generate_secret_string_payload(gen: &serde_json::Value) -> Result<String, String> {
18055 let length = gen
18056 .get("PasswordLength")
18057 .and_then(|v| v.as_i64())
18058 .unwrap_or(32) as usize;
18059 let exclude_lowercase = gen
18060 .get("ExcludeLowercase")
18061 .and_then(|v| v.as_bool())
18062 .unwrap_or(false);
18063 let exclude_uppercase = gen
18064 .get("ExcludeUppercase")
18065 .and_then(|v| v.as_bool())
18066 .unwrap_or(false);
18067 let exclude_numbers = gen
18068 .get("ExcludeNumbers")
18069 .and_then(|v| v.as_bool())
18070 .unwrap_or(false);
18071 let exclude_punctuation = gen
18072 .get("ExcludePunctuation")
18073 .and_then(|v| v.as_bool())
18074 .unwrap_or(false);
18075 let include_space = gen
18076 .get("IncludeSpace")
18077 .and_then(|v| v.as_bool())
18078 .unwrap_or(false);
18079 let exclude_chars = gen
18080 .get("ExcludeCharacters")
18081 .and_then(|v| v.as_str())
18082 .unwrap_or("")
18083 .to_string();
18084
18085 let lowercase = "abcdefghijklmnopqrstuvwxyz";
18086 let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
18087 let digits = "0123456789";
18088 let punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
18089
18090 let mut pool = String::new();
18091 if !exclude_lowercase {
18092 pool.extend(lowercase.chars().filter(|c| !exclude_chars.contains(*c)));
18093 }
18094 if !exclude_uppercase {
18095 pool.extend(uppercase.chars().filter(|c| !exclude_chars.contains(*c)));
18096 }
18097 if !exclude_numbers {
18098 pool.extend(digits.chars().filter(|c| !exclude_chars.contains(*c)));
18099 }
18100 if !exclude_punctuation {
18101 pool.extend(punctuation.chars().filter(|c| !exclude_chars.contains(*c)));
18102 }
18103 if include_space && !exclude_chars.contains(' ') {
18104 pool.push(' ');
18105 }
18106 if pool.is_empty() {
18107 return Err("GenerateSecretString character pool is empty".to_string());
18108 }
18109
18110 let pool_chars: Vec<char> = pool.chars().collect();
18111 let mut password = String::with_capacity(length);
18112 let mut counter: u64 = std::time::SystemTime::now()
18113 .duration_since(std::time::UNIX_EPOCH)
18114 .map(|d| d.as_nanos() as u64)
18115 .unwrap_or(0);
18116 while password.len() < length {
18117 counter = counter.wrapping_add(0x9E3779B97F4A7C15);
18120 let mut z = counter;
18121 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
18122 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
18123 z ^= z >> 31;
18124 let idx = (z as usize) % pool_chars.len();
18125 password.push(pool_chars[idx]);
18126 }
18127
18128 let template = gen.get("SecretStringTemplate").and_then(|v| v.as_str());
18129 let key = gen.get("GenerateStringKey").and_then(|v| v.as_str());
18130 match (template, key) {
18131 (Some(tmpl), Some(k)) => {
18132 let mut value: serde_json::Value = serde_json::from_str(tmpl)
18133 .map_err(|e| format!("SecretStringTemplate is not valid JSON: {e}"))?;
18134 if let Some(obj) = value.as_object_mut() {
18135 obj.insert(k.to_string(), serde_json::Value::String(password));
18136 Ok(value.to_string())
18137 } else {
18138 Err("SecretStringTemplate must be a JSON object".to_string())
18139 }
18140 }
18141 _ => Ok(password),
18142 }
18143}
18144
18145fn parse_ses_receipt_action(value: &serde_json::Value) -> Option<SesReceiptAction> {
18146 let obj = value.as_object()?;
18147 if let Some(s3) = obj.get("S3Action").and_then(|v| v.as_object()) {
18148 let bucket_name = s3.get("BucketName").and_then(|v| v.as_str())?.to_string();
18149 return Some(SesReceiptAction::S3 {
18150 bucket_name,
18151 object_key_prefix: s3
18152 .get("ObjectKeyPrefix")
18153 .and_then(|v| v.as_str())
18154 .map(String::from),
18155 topic_arn: s3
18156 .get("TopicArn")
18157 .and_then(|v| v.as_str())
18158 .map(String::from),
18159 kms_key_arn: s3
18160 .get("KmsKeyArn")
18161 .and_then(|v| v.as_str())
18162 .map(String::from),
18163 });
18164 }
18165 if let Some(sns) = obj.get("SNSAction").and_then(|v| v.as_object()) {
18166 return Some(SesReceiptAction::Sns {
18167 topic_arn: sns.get("TopicArn").and_then(|v| v.as_str())?.to_string(),
18168 encoding: sns
18169 .get("Encoding")
18170 .and_then(|v| v.as_str())
18171 .map(String::from),
18172 });
18173 }
18174 if let Some(la) = obj.get("LambdaAction").and_then(|v| v.as_object()) {
18175 return Some(SesReceiptAction::Lambda {
18176 function_arn: la.get("FunctionArn").and_then(|v| v.as_str())?.to_string(),
18177 invocation_type: la
18178 .get("InvocationType")
18179 .and_then(|v| v.as_str())
18180 .map(String::from),
18181 topic_arn: la
18182 .get("TopicArn")
18183 .and_then(|v| v.as_str())
18184 .map(String::from),
18185 });
18186 }
18187 if let Some(b) = obj.get("BounceAction").and_then(|v| v.as_object()) {
18188 return Some(SesReceiptAction::Bounce {
18189 smtp_reply_code: b
18190 .get("SmtpReplyCode")
18191 .and_then(|v| v.as_str())
18192 .unwrap_or("550")
18193 .to_string(),
18194 message: b
18195 .get("Message")
18196 .and_then(|v| v.as_str())
18197 .unwrap_or("")
18198 .to_string(),
18199 sender: b
18200 .get("Sender")
18201 .and_then(|v| v.as_str())
18202 .unwrap_or("")
18203 .to_string(),
18204 status_code: b
18205 .get("StatusCode")
18206 .and_then(|v| v.as_str())
18207 .map(String::from),
18208 topic_arn: b.get("TopicArn").and_then(|v| v.as_str()).map(String::from),
18209 });
18210 }
18211 if let Some(ah) = obj.get("AddHeaderAction").and_then(|v| v.as_object()) {
18212 return Some(SesReceiptAction::AddHeader {
18213 header_name: ah.get("HeaderName").and_then(|v| v.as_str())?.to_string(),
18214 header_value: ah.get("HeaderValue").and_then(|v| v.as_str())?.to_string(),
18215 });
18216 }
18217 if let Some(s) = obj.get("StopAction").and_then(|v| v.as_object()) {
18218 return Some(SesReceiptAction::Stop {
18219 scope: s
18220 .get("Scope")
18221 .and_then(|v| v.as_str())
18222 .unwrap_or("RuleSet")
18223 .to_string(),
18224 topic_arn: s.get("TopicArn").and_then(|v| v.as_str()).map(String::from),
18225 });
18226 }
18227 None
18228}
18229
18230fn make_apigwv2_id(n: usize) -> String {
18234 let s = uuid::Uuid::new_v4().simple().to_string();
18235 s[..n.min(s.len())].to_string()
18236}
18237
18238fn cfn_as_i64(v: &serde_json::Value) -> Option<i64> {
18248 if let Some(n) = v.as_i64() {
18249 return Some(n);
18250 }
18251 v.as_str().and_then(|s| s.parse::<i64>().ok())
18252}
18253
18254fn lowercase_first_keys(value: serde_json::Value) -> serde_json::Value {
18255 match value {
18256 serde_json::Value::Object(map) => {
18257 let mut out = serde_json::Map::new();
18258 for (k, v) in map {
18259 let new_key = if let Some(first) = k.chars().next() {
18260 let mut s = String::with_capacity(k.len());
18261 s.extend(first.to_lowercase());
18262 s.push_str(&k[first.len_utf8()..]);
18263 s
18264 } else {
18265 k
18266 };
18267 out.insert(new_key, lowercase_first_keys(v));
18268 }
18269 serde_json::Value::Object(out)
18270 }
18271 serde_json::Value::Array(arr) => {
18272 serde_json::Value::Array(arr.into_iter().map(lowercase_first_keys).collect())
18273 }
18274 other => other,
18275 }
18276}
18277
18278fn synth_acm_domain_validation(
18285 domain_name: &str,
18286 sans: &[String],
18287 validation_method: &str,
18288) -> Vec<AcmDomainValidation> {
18289 let mut all = vec![domain_name.to_string()];
18290 for s in sans {
18291 if !all.contains(s) {
18292 all.push(s.clone());
18293 }
18294 }
18295 all.into_iter()
18296 .map(|name| AcmDomainValidation {
18297 domain_name: name.clone(),
18298 validation_status: "SUCCESS".to_string(),
18299 validation_method: validation_method.to_string(),
18300 resource_record_name: Some(format!("_amzn-validations.{name}.")),
18301 resource_record_type: Some("CNAME".to_string()),
18302 resource_record_value: Some(format!("{}.acm-validations.aws.", Uuid::new_v4())),
18303 })
18304 .collect()
18305}
18306
18307fn parse_acm_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
18309 let mut out = BTreeMap::new();
18310 if let Some(arr) = value.and_then(|v| v.as_array()) {
18311 for t in arr {
18312 if let (Some(k), Some(v)) = (
18313 t.get("Key").and_then(|v| v.as_str()),
18314 t.get("Value").and_then(|v| v.as_str()),
18315 ) {
18316 out.insert(k.to_string(), v.to_string());
18317 }
18318 }
18319 }
18320 out
18321}
18322
18323fn parse_ecs_tags(value: Option<&serde_json::Value>) -> Vec<EcsTagEntry> {
18325 let Some(arr) = value.and_then(|v| v.as_array()) else {
18326 return Vec::new();
18327 };
18328 arr.iter()
18329 .filter_map(|t| {
18330 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
18331 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
18332 Some(EcsTagEntry { key, value })
18333 })
18334 .collect()
18335}
18336
18337fn parse_ecs_cluster_name(input: &str) -> String {
18340 if let Some(after) = input.split(":cluster/").nth(1) {
18341 return after.to_string();
18342 }
18343 input.to_string()
18344}
18345
18346fn parse_td_arn(input: &str) -> (String, i32) {
18350 let suffix = input.rsplit('/').next().unwrap_or(input);
18351 if let Some((family, rev)) = suffix.split_once(':') {
18352 if let Ok(revision) = rev.parse::<i32>() {
18353 return (family.to_string(), revision);
18354 }
18355 }
18356 (input.to_string(), 1)
18357}
18358
18359fn parse_service_arn(input: &str) -> Option<(String, String)> {
18362 let after = input.split(":service/").nth(1)?;
18363 let mut parts = after.splitn(2, '/');
18364 let cluster = parts.next()?.to_string();
18365 let service = parts.next()?.to_string();
18366 Some((cluster, service))
18367}
18368
18369fn parse_rds_tags(value: Option<&serde_json::Value>) -> Vec<RdsTag> {
18371 let Some(arr) = value.and_then(|v| v.as_array()) else {
18372 return Vec::new();
18373 };
18374 arr.iter()
18375 .filter_map(|t| {
18376 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
18377 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
18378 Some(RdsTag { key, value })
18379 })
18380 .collect()
18381}
18382
18383fn rds_extras_mut<'a>(
18387 state: &'a mut fakecloud_rds::RdsState,
18388 category: &str,
18389) -> &'a mut BTreeMap<String, serde_json::Value> {
18390 state.extras.entry(category.to_string()).or_default()
18391}
18392
18393fn parse_cognito_string_array(value: Option<&serde_json::Value>) -> Vec<String> {
18397 value
18398 .and_then(|v| v.as_array())
18399 .map(|arr| {
18400 arr.iter()
18401 .filter_map(|v| v.as_str().map(|s| s.to_string()))
18402 .collect()
18403 })
18404 .unwrap_or_default()
18405}
18406
18407fn parse_cognito_password_policy(value: Option<&serde_json::Value>) -> PasswordPolicy {
18408 let Some(inner) = value
18409 .and_then(|v| v.get("PasswordPolicy"))
18410 .and_then(|v| v.as_object())
18411 else {
18412 return PasswordPolicy::default();
18413 };
18414 let mut p = PasswordPolicy::default();
18415 if let Some(n) = inner.get("MinimumLength").and_then(|v| v.as_i64()) {
18416 p.minimum_length = n;
18417 }
18418 if let Some(b) = inner.get("RequireUppercase").and_then(|v| v.as_bool()) {
18419 p.require_uppercase = b;
18420 }
18421 if let Some(b) = inner.get("RequireLowercase").and_then(|v| v.as_bool()) {
18422 p.require_lowercase = b;
18423 }
18424 if let Some(b) = inner.get("RequireNumbers").and_then(|v| v.as_bool()) {
18425 p.require_numbers = b;
18426 }
18427 if let Some(b) = inner.get("RequireSymbols").and_then(|v| v.as_bool()) {
18428 p.require_symbols = b;
18429 }
18430 if let Some(n) = inner
18431 .get("TemporaryPasswordValidityDays")
18432 .and_then(|v| v.as_i64())
18433 {
18434 p.temporary_password_validity_days = n;
18435 }
18436 p
18437}
18438
18439fn parse_cognito_schema_attribute(value: &serde_json::Value) -> Option<SchemaAttribute> {
18440 let name = value.get("Name").and_then(|v| v.as_str())?.to_string();
18441 Some(SchemaAttribute {
18442 name,
18443 attribute_data_type: value
18444 .get("AttributeDataType")
18445 .and_then(|v| v.as_str())
18446 .unwrap_or("String")
18447 .to_string(),
18448 developer_only_attribute: value
18449 .get("DeveloperOnlyAttribute")
18450 .and_then(|v| v.as_bool())
18451 .unwrap_or(false),
18452 mutable: value
18453 .get("Mutable")
18454 .and_then(|v| v.as_bool())
18455 .unwrap_or(true),
18456 required: value
18457 .get("Required")
18458 .and_then(|v| v.as_bool())
18459 .unwrap_or(false),
18460 string_attribute_constraints: None,
18461 number_attribute_constraints: None,
18462 })
18463}
18464
18465fn parse_cognito_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
18466 let mut out = BTreeMap::new();
18467 if let Some(obj) = value.and_then(|v| v.as_object()) {
18468 for (k, v) in obj {
18469 if let Some(s) = v.as_str() {
18470 out.insert(k.clone(), s.to_string());
18471 }
18472 }
18473 }
18474 out
18475}
18476
18477fn parse_cognito_email_configuration(
18478 value: Option<&serde_json::Value>,
18479) -> Option<EmailConfiguration> {
18480 let inner = value?.as_object()?;
18481 Some(EmailConfiguration {
18482 source_arn: inner
18483 .get("SourceArn")
18484 .and_then(|v| v.as_str())
18485 .map(|s| s.to_string()),
18486 reply_to_email_address: inner
18487 .get("ReplyToEmailAddress")
18488 .and_then(|v| v.as_str())
18489 .map(|s| s.to_string()),
18490 email_sending_account: inner
18491 .get("EmailSendingAccount")
18492 .and_then(|v| v.as_str())
18493 .map(|s| s.to_string()),
18494 from_email_address: inner
18495 .get("From")
18496 .and_then(|v| v.as_str())
18497 .map(|s| s.to_string()),
18498 configuration_set: inner
18499 .get("ConfigurationSet")
18500 .and_then(|v| v.as_str())
18501 .map(|s| s.to_string()),
18502 })
18503}
18504
18505fn parse_cognito_sms_configuration(value: Option<&serde_json::Value>) -> Option<SmsConfiguration> {
18506 let inner = value?.as_object()?;
18507 Some(SmsConfiguration {
18508 sns_caller_arn: inner
18509 .get("SnsCallerArn")
18510 .and_then(|v| v.as_str())
18511 .map(|s| s.to_string()),
18512 external_id: inner
18513 .get("ExternalId")
18514 .and_then(|v| v.as_str())
18515 .map(|s| s.to_string()),
18516 sns_region: inner
18517 .get("SnsRegion")
18518 .and_then(|v| v.as_str())
18519 .map(|s| s.to_string()),
18520 })
18521}
18522
18523fn parse_cognito_admin_create_user_config(
18524 value: Option<&serde_json::Value>,
18525) -> Option<AdminCreateUserConfig> {
18526 let inner = value?.as_object()?;
18527 Some(AdminCreateUserConfig {
18528 allow_admin_create_user_only: inner
18529 .get("AllowAdminCreateUserOnly")
18530 .and_then(|v| v.as_bool()),
18531 invite_message_template: None,
18532 unused_account_validity_days: inner
18533 .get("UnusedAccountValidityDays")
18534 .and_then(|v| v.as_i64()),
18535 })
18536}
18537
18538fn parse_cognito_account_recovery(
18539 value: Option<&serde_json::Value>,
18540) -> Option<AccountRecoverySetting> {
18541 let arr = value?.get("RecoveryMechanisms")?.as_array()?;
18542 Some(AccountRecoverySetting {
18543 recovery_mechanisms: arr
18544 .iter()
18545 .filter_map(|m| {
18546 let name = m.get("Name").and_then(|v| v.as_str())?.to_string();
18547 let priority = m.get("Priority").and_then(|v| v.as_i64()).unwrap_or(1);
18548 Some(RecoveryOption { name, priority })
18549 })
18550 .collect(),
18551 })
18552}
18553
18554fn parse_firehose_s3_destination(value: &serde_json::Value) -> Result<S3Destination, String> {
18555 let role_arn = value
18556 .get("RoleARN")
18557 .and_then(|v| v.as_str())
18558 .ok_or("S3 destination requires RoleARN")?
18559 .to_string();
18560 let bucket_arn = value
18561 .get("BucketARN")
18562 .and_then(|v| v.as_str())
18563 .ok_or("S3 destination requires BucketARN")?
18564 .to_string();
18565 let prefix = value
18566 .get("Prefix")
18567 .and_then(|v| v.as_str())
18568 .map(|s| s.to_string());
18569 let error_output_prefix = value
18570 .get("ErrorOutputPrefix")
18571 .and_then(|v| v.as_str())
18572 .map(|s| s.to_string());
18573 let mut buffering_size_mb = None;
18574 let mut buffering_interval_seconds = None;
18575 if let Some(hints) = value.get("BufferingHints") {
18576 buffering_size_mb = hints.get("SizeInMBs").and_then(|v| v.as_i64());
18577 buffering_interval_seconds = hints.get("IntervalInSeconds").and_then(|v| v.as_i64());
18578 }
18579 let compression_format = value
18580 .get("CompressionFormat")
18581 .and_then(|v| v.as_str())
18582 .map(|s| s.to_string());
18583
18584 Ok(S3Destination {
18585 destination_id: "destination-1".to_string(),
18586 role_arn,
18587 bucket_arn,
18588 prefix,
18589 error_output_prefix,
18590 buffering_size_mb,
18591 buffering_interval_seconds,
18592 compression_format,
18593 })
18594}
18595
18596#[cfg(test)]
18597mod tests {
18598 use super::*;
18599 use parking_lot::RwLock;
18600
18601 fn make_provisioner() -> ResourceProvisioner {
18602 ResourceProvisioner {
18603 sqs_state: Arc::new(RwLock::new(
18604 fakecloud_core::multi_account::MultiAccountState::new(
18605 "123456789012",
18606 "us-east-1",
18607 "http://localhost:4566",
18608 ),
18609 )),
18610 sns_state: Arc::new(RwLock::new(
18611 fakecloud_core::multi_account::MultiAccountState::new(
18612 "123456789012",
18613 "us-east-1",
18614 "http://localhost:4566",
18615 ),
18616 )),
18617 ssm_state: Arc::new(RwLock::new(
18618 fakecloud_core::multi_account::MultiAccountState::new(
18619 "123456789012",
18620 "us-east-1",
18621 "http://localhost:4566",
18622 ),
18623 )),
18624 iam_state: Arc::new(RwLock::new(
18625 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "http://localhost:4566"),
18626 )),
18627 s3_state: Arc::new(RwLock::new(fakecloud_core::multi_account::MultiAccountState::new(
18628 "123456789012",
18629 "us-east-1", "",
18630 ))),
18631 eventbridge_state: Arc::new(RwLock::new(
18632 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18633 )),
18634 dynamodb_state: Arc::new(RwLock::new(fakecloud_core::multi_account::MultiAccountState::new(
18635 "123456789012",
18636 "us-east-1", "",
18637 ))),
18638 logs_state: Arc::new(RwLock::new(
18639 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18640 )),
18641 lambda_state: Arc::new(RwLock::new(
18642 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18643 )),
18644 secretsmanager_state: Arc::new(RwLock::new(
18645 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18646 )),
18647 kinesis_state: Arc::new(RwLock::new(
18648 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18649 )),
18650 kms_state: Arc::new(RwLock::new(
18651 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18652 )),
18653 ecr_state: Arc::new(RwLock::new(
18654 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18655 )),
18656 cloudwatch_state: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
18657 elbv2_state: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
18658 organizations_state: Arc::new(RwLock::new(None)),
18659 cognito_state: Arc::new(RwLock::new(
18660 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18661 )),
18662 rds_state: Arc::new(RwLock::new(
18663 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18664 )),
18665 ecs_state: Arc::new(RwLock::new(
18666 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18667 )),
18668 acm_state: Arc::new(RwLock::new(fakecloud_acm::AcmAccounts::new())),
18669 elasticache_state: Arc::new(RwLock::new(
18670 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18671 )),
18672 route53_state: Arc::new(RwLock::new(fakecloud_route53::Route53Accounts::new())),
18673 cloudfront_state: Arc::new(RwLock::new(
18674 fakecloud_cloudfront::CloudFrontAccounts::new(),
18675 )),
18676 cloudformation_state: Arc::new(RwLock::new(
18677 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18678 )),
18679 stepfunctions_state: Arc::new(RwLock::new(
18680 fakecloud_core::multi_account::MultiAccountState::new(
18681 "123456789012",
18682 "us-east-1",
18683 "",
18684 ),
18685 )),
18686 wafv2_state: Arc::new(RwLock::new(fakecloud_wafv2::Wafv2Accounts::default())),
18687 apigateway_state: Arc::new(RwLock::new(
18688 fakecloud_core::multi_account::MultiAccountState::new(
18689 "123456789012",
18690 "us-east-1",
18691 "",
18692 ),
18693 )),
18694 apigatewayv2_state: Arc::new(RwLock::new(
18695 fakecloud_core::multi_account::MultiAccountState::new(
18696 "123456789012",
18697 "us-east-1",
18698 "",
18699 ),
18700 )),
18701 ses_state: Arc::new(RwLock::new(
18702 fakecloud_core::multi_account::MultiAccountState::new(
18703 "123456789012",
18704 "us-east-1",
18705 "",
18706 ),
18707 )),
18708 app_autoscaling_state: Arc::new(parking_lot::RwLock::new(
18709 fakecloud_application_autoscaling::ApplicationAutoScalingAccounts::new(),
18710 )),
18711 athena_state: Arc::new(parking_lot::RwLock::new(
18712 fakecloud_athena::AthenaAccounts::new(),
18713 )),
18714 firehose_state: Arc::new(parking_lot::RwLock::new(
18715 fakecloud_firehose::FirehoseAccounts::new(),
18716 )),
18717 glue_state: Arc::new(parking_lot::RwLock::new(
18718 fakecloud_glue::GlueAccounts::new(),
18719 )),
18720 delivery: Arc::new(DeliveryBus::new()),
18721 account_id: "123456789012".to_string(),
18722 region: "us-east-1".to_string(),
18723 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test/00000000-0000-0000-0000-000000000000".to_string(),
18724 }
18725 }
18726
18727 fn make_resource(
18728 resource_type: &str,
18729 logical_id: &str,
18730 props: serde_json::Value,
18731 ) -> ResourceDefinition {
18732 ResourceDefinition {
18733 logical_id: logical_id.to_string(),
18734 resource_type: resource_type.to_string(),
18735 properties: props,
18736 }
18737 }
18738
18739 #[test]
18740 fn sns_subscription_rejects_nonexistent_topic() {
18741 let prov = make_provisioner();
18742 let resource = make_resource(
18743 "AWS::SNS::Subscription",
18744 "MySub",
18745 serde_json::json!({
18746 "TopicArn": "arn:aws:sns:us-east-1:123456789012:NonExistent",
18747 "Protocol": "sqs",
18748 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:my-queue"
18749 }),
18750 );
18751 let result = prov.create_resource(&resource);
18752 assert!(result.is_err());
18753 assert!(result.unwrap_err().contains("does not exist"));
18754 }
18755
18756 #[test]
18757 fn sns_subscription_succeeds_when_topic_exists() {
18758 let prov = make_provisioner();
18759 let topic = make_resource(
18761 "AWS::SNS::Topic",
18762 "MyTopic",
18763 serde_json::json!({ "TopicName": "my-topic" }),
18764 );
18765 let topic_result = prov.create_resource(&topic);
18766 assert!(topic_result.is_ok());
18767 let topic_arn = topic_result.unwrap().physical_id;
18768
18769 let sub = make_resource(
18771 "AWS::SNS::Subscription",
18772 "MySub",
18773 serde_json::json!({
18774 "TopicArn": topic_arn,
18775 "Protocol": "sqs",
18776 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:my-queue"
18777 }),
18778 );
18779 let result = prov.create_resource(&sub);
18780 assert!(result.is_ok());
18781 }
18782
18783 #[test]
18784 fn eventbridge_rule_arn_default_bus_omits_bus_name() {
18785 let prov = make_provisioner();
18786 let resource = make_resource(
18787 "AWS::Events::Rule",
18788 "MyRule",
18789 serde_json::json!({
18790 "Name": "my-rule",
18791 "ScheduleExpression": "rate(1 hour)"
18792 }),
18793 );
18794 let result = prov.create_resource(&resource).unwrap();
18795 assert_eq!(
18797 result.physical_id,
18798 "arn:aws:events:us-east-1:123456789012:rule/my-rule"
18799 );
18800 assert!(!result.physical_id.contains("rule/default/"));
18801 }
18802
18803 #[test]
18804 fn eventbridge_rule_arn_custom_bus_includes_bus_name() {
18805 let prov = make_provisioner();
18806 {
18808 let mut eb_accounts = prov.eventbridge_state.write();
18809 let state = eb_accounts.default_mut();
18810 state.buses.insert(
18811 "custom-bus".to_string(),
18812 fakecloud_eventbridge::EventBus {
18813 name: "custom-bus".to_string(),
18814 arn: "arn:aws:events:us-east-1:123456789012:event-bus/custom-bus".to_string(),
18815 policy: None,
18816 creation_time: Utc::now(),
18817 last_modified_time: Utc::now(),
18818 description: None,
18819 kms_key_identifier: None,
18820 dead_letter_config: None,
18821 tags: std::collections::BTreeMap::new(),
18822 },
18823 );
18824 }
18825 let resource = make_resource(
18826 "AWS::Events::Rule",
18827 "MyRule",
18828 serde_json::json!({
18829 "Name": "my-rule",
18830 "EventBusName": "custom-bus",
18831 "ScheduleExpression": "rate(1 hour)"
18832 }),
18833 );
18834 let result = prov.create_resource(&resource).unwrap();
18835 assert_eq!(
18836 result.physical_id,
18837 "arn:aws:events:us-east-1:123456789012:rule/custom-bus/my-rule"
18838 );
18839 }
18840
18841 #[test]
18842 fn eventbridge_rule_rejects_nonexistent_bus() {
18843 let prov = make_provisioner();
18844 let resource = make_resource(
18845 "AWS::Events::Rule",
18846 "MyRule",
18847 serde_json::json!({
18848 "Name": "my-rule",
18849 "EventBusName": "nonexistent-bus",
18850 "ScheduleExpression": "rate(1 hour)"
18851 }),
18852 );
18853 let result = prov.create_resource(&resource);
18854 assert!(result.is_err());
18855 assert!(result.unwrap_err().contains("does not exist"));
18856 }
18857
18858 #[test]
18859 fn custom_resource_requires_service_token() {
18860 let prov = make_provisioner();
18861 let resource = make_resource(
18862 "Custom::MyResource",
18863 "MyCustom",
18864 serde_json::json!({
18865 "Foo": "bar"
18866 }),
18867 );
18868 let result = prov.create_resource(&resource);
18869 assert!(result.is_err());
18870 assert!(
18871 result.unwrap_err().contains("ServiceToken"),
18872 "Should require ServiceToken property"
18873 );
18874 }
18875
18876 #[test]
18877 fn custom_resource_succeeds_without_lambda_delivery() {
18878 let prov = make_provisioner();
18881 let resource = make_resource(
18882 "Custom::MyResource",
18883 "MyCustom",
18884 serde_json::json!({
18885 "ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:my-func",
18886 "Foo": "bar"
18887 }),
18888 );
18889 let result = prov.create_resource(&resource);
18890 assert!(result.is_ok());
18891 let sr = result.unwrap();
18892 assert_eq!(sr.logical_id, "MyCustom");
18893 assert_eq!(sr.resource_type, "Custom::MyResource");
18894 assert!(sr.physical_id.starts_with("MyCustom-"));
18895 }
18896
18897 #[test]
18898 fn cloudformation_custom_resource_type_succeeds() {
18899 let prov = make_provisioner();
18900 let resource = make_resource(
18901 "AWS::CloudFormation::CustomResource",
18902 "MyCustom2",
18903 serde_json::json!({
18904 "ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:my-func",
18905 "Key": "value"
18906 }),
18907 );
18908 let result = prov.create_resource(&resource);
18909 assert!(result.is_ok());
18910 let sr = result.unwrap();
18911 assert_eq!(sr.resource_type, "AWS::CloudFormation::CustomResource");
18912 }
18913
18914 #[test]
18917 fn sqs_queue_create_and_delete() {
18918 let prov = make_provisioner();
18919 let res = make_resource(
18920 "AWS::SQS::Queue",
18921 "MyQ",
18922 serde_json::json!({"QueueName": "my-q"}),
18923 );
18924 let sr = prov.create_resource(&res).unwrap();
18925 assert!(sr.physical_id.contains("my-q"));
18926 assert_eq!(sr.resource_type, "AWS::SQS::Queue");
18927 prov.delete_resource(&sr).unwrap();
18928 }
18929
18930 #[test]
18931 fn sqs_queue_fifo_with_suffix() {
18932 let prov = make_provisioner();
18933 let res = make_resource(
18934 "AWS::SQS::Queue",
18935 "FifoQ",
18936 serde_json::json!({"QueueName": "my-fifo.fifo", "FifoQueue": true}),
18937 );
18938 let sr = prov.create_resource(&res).unwrap();
18939 assert!(sr.physical_id.contains(".fifo"));
18940 }
18941
18942 #[test]
18943 fn sns_topic_create_and_delete() {
18944 let prov = make_provisioner();
18945 let res = make_resource(
18946 "AWS::SNS::Topic",
18947 "MyTopic",
18948 serde_json::json!({"TopicName": "t1"}),
18949 );
18950 let sr = prov.create_resource(&res).unwrap();
18951 assert!(sr.physical_id.contains("t1"));
18952 prov.delete_resource(&sr).unwrap();
18953 }
18954
18955 #[test]
18956 fn ssm_parameter_create_and_delete() {
18957 let prov = make_provisioner();
18958 let res = make_resource(
18959 "AWS::SSM::Parameter",
18960 "MyParam",
18961 serde_json::json!({
18962 "Name": "/my/param",
18963 "Type": "String",
18964 "Value": "v1"
18965 }),
18966 );
18967 let sr = prov.create_resource(&res).unwrap();
18968 assert_eq!(sr.physical_id, "/my/param");
18969 prov.delete_resource(&sr).unwrap();
18970 }
18971
18972 #[test]
18973 fn iam_role_create_and_delete() {
18974 let prov = make_provisioner();
18975 let res = make_resource(
18976 "AWS::IAM::Role",
18977 "MyRole",
18978 serde_json::json!({
18979 "RoleName": "my-role",
18980 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
18981 }),
18982 );
18983 let sr = prov.create_resource(&res).unwrap();
18984 assert!(sr.physical_id.contains("my-role"));
18985 prov.delete_resource(&sr).unwrap();
18986 }
18987
18988 #[test]
18989 fn iam_policy_create_and_delete() {
18990 let prov = make_provisioner();
18991 let res = make_resource(
18992 "AWS::IAM::Policy",
18993 "MyPolicy",
18994 serde_json::json!({
18995 "PolicyName": "my-policy",
18996 "PolicyDocument": {"Version": "2012-10-17", "Statement": []}
18997 }),
18998 );
18999 let sr = prov.create_resource(&res).unwrap();
19000 assert!(sr.physical_id.contains("my-policy"));
19001 prov.delete_resource(&sr).unwrap();
19002 }
19003
19004 #[test]
19005 fn s3_bucket_create_and_delete() {
19006 let prov = make_provisioner();
19007 let res = make_resource(
19008 "AWS::S3::Bucket",
19009 "MyBucket",
19010 serde_json::json!({"BucketName": "my-bucket"}),
19011 );
19012 let sr = prov.create_resource(&res).unwrap();
19013 assert_eq!(sr.physical_id, "my-bucket");
19014 prov.delete_resource(&sr).unwrap();
19015 }
19016
19017 #[test]
19018 fn dynamodb_table_create_and_delete() {
19019 let prov = make_provisioner();
19020 let res = make_resource(
19021 "AWS::DynamoDB::Table",
19022 "MyTable",
19023 serde_json::json!({
19024 "TableName": "my-table",
19025 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
19026 "AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
19027 "BillingMode": "PAY_PER_REQUEST"
19028 }),
19029 );
19030 let sr = prov.create_resource(&res).unwrap();
19031 assert!(sr.physical_id.contains("my-table"));
19032 prov.delete_resource(&sr).unwrap();
19033 }
19034
19035 #[test]
19036 fn log_group_create_and_delete() {
19037 let prov = make_provisioner();
19038 let res = make_resource(
19039 "AWS::Logs::LogGroup",
19040 "MyLogs",
19041 serde_json::json!({"LogGroupName": "/app/logs"}),
19042 );
19043 let sr = prov.create_resource(&res).unwrap();
19044 assert!(sr.physical_id.contains("/app/logs"));
19045 prov.delete_resource(&sr).unwrap();
19046 }
19047
19048 #[test]
19049 fn lambda_function_create_and_delete() {
19050 let prov = make_provisioner();
19051 let res = make_resource(
19052 "AWS::Lambda::Function",
19053 "MyFn",
19054 serde_json::json!({
19055 "FunctionName": "my-fn",
19056 "Runtime": "nodejs20.x",
19057 "Role": "arn:aws:iam::123456789012:role/lambda-role",
19058 "Handler": "index.handler",
19059 "MemorySize": 256,
19060 "Timeout": 10,
19061 "Environment": {"Variables": {"FOO": "bar"}}
19062 }),
19063 );
19064 let sr = prov.create_resource(&res).unwrap();
19065 assert_eq!(sr.physical_id, "my-fn");
19066 assert_eq!(
19067 sr.attributes.get("Arn").map(String::as_str),
19068 Some("arn:aws:lambda:us-east-1:123456789012:function:my-fn")
19069 );
19070 {
19072 let lam = prov.lambda_state.read();
19073 let st = lam.get("123456789012").unwrap();
19074 let f = st.functions.get("my-fn").unwrap();
19075 assert_eq!(f.runtime, "nodejs20.x");
19076 assert_eq!(f.memory_size, 256);
19077 assert_eq!(f.environment.get("FOO").unwrap(), "bar");
19078 }
19079 prov.delete_resource(&sr).unwrap();
19080 let lam = prov.lambda_state.read();
19081 let st = lam.get("123456789012").unwrap();
19082 assert!(!st.functions.contains_key("my-fn"));
19083 }
19084
19085 #[test]
19086 fn unsupported_resource_type_fails() {
19087 let prov = make_provisioner();
19088 let res = make_resource("AWS::NonExistent::Thing", "X", serde_json::json!({}));
19089 assert!(prov.create_resource(&res).is_err());
19090 }
19091
19092 #[test]
19093 fn iam_role_with_inline_policies() {
19094 let prov = make_provisioner();
19095 let res = make_resource(
19096 "AWS::IAM::Role",
19097 "MyRole",
19098 serde_json::json!({
19099 "RoleName": "role-inline",
19100 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []},
19101 "Policies": [
19102 {
19103 "PolicyName": "inline-1",
19104 "PolicyDocument": {"Version": "2012-10-17", "Statement": []}
19105 }
19106 ]
19107 }),
19108 );
19109 let sr = prov.create_resource(&res).unwrap();
19110 assert!(sr.physical_id.contains("role-inline"));
19111 }
19112
19113 #[test]
19114 fn sqs_queue_auto_name() {
19115 let prov = make_provisioner();
19116 let res = make_resource("AWS::SQS::Queue", "AutoQ", serde_json::json!({}));
19117 let sr = prov.create_resource(&res).unwrap();
19118 assert!(!sr.physical_id.is_empty());
19120 }
19121
19122 #[test]
19123 fn sns_topic_auto_name() {
19124 let prov = make_provisioner();
19125 let res = make_resource("AWS::SNS::Topic", "AutoT", serde_json::json!({}));
19126 let sr = prov.create_resource(&res).unwrap();
19127 assert!(!sr.physical_id.is_empty());
19128 }
19129
19130 #[test]
19133 fn unsupported_resource_type_errors() {
19134 let prov = make_provisioner();
19135 let res = make_resource("AWS::FooBar::Thing", "X", serde_json::json!({}));
19136 assert!(prov.create_resource(&res).is_err());
19137 }
19138
19139 #[test]
19140 fn sqs_queue_with_redrive_policy() {
19141 let prov = make_provisioner();
19142 let dlq = make_resource(
19144 "AWS::SQS::Queue",
19145 "DLQ",
19146 serde_json::json!({"QueueName": "dlq1"}),
19147 );
19148 let dlq_resource = prov.create_resource(&dlq).unwrap();
19149 let _ = dlq_resource.physical_id;
19150
19151 let src = make_resource(
19153 "AWS::SQS::Queue",
19154 "Src",
19155 serde_json::json!({
19156 "QueueName": "src1",
19157 "RedrivePolicy": {
19158 "deadLetterTargetArn": "arn:aws:sqs:us-east-1:123456789012:dlq1",
19159 "maxReceiveCount": 3
19160 }
19161 }),
19162 );
19163 let sr = prov.create_resource(&src).unwrap();
19164 assert!(!sr.physical_id.is_empty());
19165 }
19166
19167 #[test]
19168 fn sns_topic_with_display_name() {
19169 let prov = make_provisioner();
19170 let res = make_resource(
19171 "AWS::SNS::Topic",
19172 "WithName",
19173 serde_json::json!({"TopicName": "named-topic", "DisplayName": "Named"}),
19174 );
19175 let sr = prov.create_resource(&res).unwrap();
19176 assert!(sr.physical_id.contains("named-topic"));
19177 }
19178
19179 #[test]
19180 fn ssm_parameter_with_explicit_name() {
19181 let prov = make_provisioner();
19182 let res = make_resource(
19183 "AWS::SSM::Parameter",
19184 "Param",
19185 serde_json::json!({"Name": "/my/param", "Value": "v", "Type": "String"}),
19186 );
19187 let sr = prov.create_resource(&res).unwrap();
19188 assert!(sr.physical_id.contains("/my/param"));
19189 }
19190
19191 #[test]
19192 fn ssm_parameter_missing_name_errors() {
19193 let prov = make_provisioner();
19194 let res = make_resource(
19195 "AWS::SSM::Parameter",
19196 "AutoP",
19197 serde_json::json!({"Value": "v", "Type": "String"}),
19198 );
19199 assert!(prov.create_resource(&res).is_err());
19200 }
19201
19202 #[test]
19203 fn iam_managed_policy_auto_name() {
19204 let prov = make_provisioner();
19205 let res = make_resource(
19206 "AWS::IAM::Policy",
19207 "AutoPol",
19208 serde_json::json!({
19209 "PolicyName": "inline-pol",
19210 "PolicyDocument": {"Version": "2012-10-17", "Statement": []},
19211 "Users": []
19212 }),
19213 );
19214 let sr = prov.create_resource(&res).unwrap();
19215 assert!(!sr.physical_id.is_empty());
19216 }
19217
19218 #[test]
19219 fn delete_resource_works_for_queue() {
19220 let prov = make_provisioner();
19221 let res = make_resource(
19222 "AWS::SQS::Queue",
19223 "ToDel",
19224 serde_json::json!({"QueueName": "todel"}),
19225 );
19226 let sr = prov.create_resource(&res).unwrap();
19227 assert!(prov.delete_resource(&sr).is_ok());
19228 }
19229
19230 #[test]
19231 fn delete_resource_works_for_topic() {
19232 let prov = make_provisioner();
19233 let res = make_resource(
19234 "AWS::SNS::Topic",
19235 "DelT",
19236 serde_json::json!({"TopicName": "delt"}),
19237 );
19238 let sr = prov.create_resource(&res).unwrap();
19239 assert!(prov.delete_resource(&sr).is_ok());
19240 }
19241
19242 #[test]
19243 fn application_autoscaling_scalable_target_round_trip() {
19244 let prov = make_provisioner();
19245 let res = make_resource(
19246 "AWS::ApplicationAutoScaling::ScalableTarget",
19247 "Target",
19248 serde_json::json!({
19249 "ServiceNamespace": "ecs",
19250 "ResourceId": "service/my-cluster/my-service",
19251 "ScalableDimension": "ecs:service:DesiredCount",
19252 "MinCapacity": 1,
19253 "MaxCapacity": 10,
19254 "RoleARN": "arn:aws:iam::123456789012:role/my-role",
19255 }),
19256 );
19257 let sr = prov.create_resource(&res).unwrap();
19258 assert_eq!(sr.physical_id, "service/my-cluster/my-service");
19259 assert!(sr.attributes.contains_key("ScalableTargetARN"));
19260 assert!(prov.delete_resource(&sr).is_ok());
19261 }
19262
19263 #[test]
19264 fn application_autoscaling_scaling_policy_requires_target() {
19265 let prov = make_provisioner();
19266 let res = make_resource(
19267 "AWS::ApplicationAutoScaling::ScalingPolicy",
19268 "Policy",
19269 serde_json::json!({
19270 "PolicyName": "my-policy",
19271 "ServiceNamespace": "ecs",
19272 "ResourceId": "service/my-cluster/my-service",
19273 "ScalableDimension": "ecs:service:DesiredCount",
19274 "PolicyType": "TargetTrackingScaling",
19275 "TargetTrackingScalingPolicyConfiguration": {
19276 "TargetValue": 50.0,
19277 "PredefinedMetricSpecification": {
19278 "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
19279 }
19280 },
19281 }),
19282 );
19283 assert!(prov.create_resource(&res).is_err());
19285 }
19286
19287 #[test]
19288 fn application_autoscaling_scaling_policy_round_trip() {
19289 let prov = make_provisioner();
19290 let target = make_resource(
19291 "AWS::ApplicationAutoScaling::ScalableTarget",
19292 "Target",
19293 serde_json::json!({
19294 "ServiceNamespace": "ecs",
19295 "ResourceId": "service/my-cluster/my-service",
19296 "ScalableDimension": "ecs:service:DesiredCount",
19297 "MinCapacity": 1,
19298 "MaxCapacity": 10,
19299 }),
19300 );
19301 let sr = prov.create_resource(&target).unwrap();
19302
19303 let policy = make_resource(
19304 "AWS::ApplicationAutoScaling::ScalingPolicy",
19305 "Policy",
19306 serde_json::json!({
19307 "PolicyName": "my-policy",
19308 "ServiceNamespace": "ecs",
19309 "ResourceId": "service/my-cluster/my-service",
19310 "ScalableDimension": "ecs:service:DesiredCount",
19311 "PolicyType": "TargetTrackingScaling",
19312 "TargetTrackingScalingPolicyConfiguration": {
19313 "TargetValue": 50.0,
19314 "PredefinedMetricSpecification": {
19315 "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
19316 }
19317 },
19318 }),
19319 );
19320 let psr = prov.create_resource(&policy).unwrap();
19321 assert!(psr.physical_id.starts_with("arn:aws:autoscaling:"));
19322 assert!(prov.delete_resource(&psr).is_ok());
19323 assert!(prov.delete_resource(&sr).is_ok());
19324 }
19325
19326 #[test]
19327 fn sqs_queue_with_fifo_suffix() {
19328 let prov = make_provisioner();
19329 let res = make_resource(
19330 "AWS::SQS::Queue",
19331 "Fifo",
19332 serde_json::json!({"QueueName": "fq.fifo", "FifoQueue": true}),
19333 );
19334 let sr = prov.create_resource(&res).unwrap();
19335 assert!(sr.physical_id.ends_with(".fifo"));
19336 }
19337
19338 #[test]
19341 fn getatt_s3_bucket_arn_returns_arn() {
19342 let prov = make_provisioner();
19343 let bucket = make_resource(
19344 "AWS::S3::Bucket",
19345 "MyBucket",
19346 serde_json::json!({"BucketName": "my-bucket"}),
19347 );
19348 let sr = prov.create_resource(&bucket).unwrap();
19349 assert_eq!(
19350 prov.get_att(&sr, "Arn"),
19351 Some("arn:aws:s3:::my-bucket".to_string())
19352 );
19353 }
19354
19355 #[test]
19356 fn getatt_s3_bucket_domain_name_returns_dns_name() {
19357 let prov = make_provisioner();
19358 let bucket = make_resource(
19359 "AWS::S3::Bucket",
19360 "MyBucket",
19361 serde_json::json!({"BucketName": "my-bucket"}),
19362 );
19363 let sr = prov.create_resource(&bucket).unwrap();
19364 assert_eq!(
19365 prov.get_att(&sr, "DomainName"),
19366 Some("my-bucket.s3.amazonaws.com".to_string())
19367 );
19368 }
19369
19370 #[test]
19371 fn getatt_lambda_function_arn_returns_function_arn() {
19372 let prov = make_provisioner();
19373 let role = make_resource(
19375 "AWS::IAM::Role",
19376 "MyRole",
19377 serde_json::json!({
19378 "RoleName": "my-role",
19379 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
19380 }),
19381 );
19382 let role_sr = prov.create_resource(&role).unwrap();
19383 let fn_res = make_resource(
19384 "AWS::Lambda::Function",
19385 "MyFn",
19386 serde_json::json!({
19387 "FunctionName": "my-fn",
19388 "Runtime": "python3.11",
19389 "Handler": "index.handler",
19390 "Role": role_sr.physical_id,
19391 "Code": {"ZipFile": "def handler(e,c): return e"}
19392 }),
19393 );
19394 let fn_sr = prov.create_resource(&fn_res).unwrap();
19395 let arn = prov.get_att(&fn_sr, "Arn").expect("Arn should resolve");
19396 assert!(arn.starts_with("arn:aws:lambda:"));
19397 assert!(arn.contains(":function:my-fn"));
19398 }
19399
19400 #[test]
19401 fn getatt_iam_role_arn_returns_role_arn() {
19402 let prov = make_provisioner();
19403 let role = make_resource(
19404 "AWS::IAM::Role",
19405 "MyRole",
19406 serde_json::json!({
19407 "RoleName": "my-role",
19408 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
19409 }),
19410 );
19411 let sr = prov.create_resource(&role).unwrap();
19412 assert_eq!(
19413 prov.get_att(&sr, "Arn"),
19414 Some("arn:aws:iam::123456789012:role/my-role".to_string())
19415 );
19416 let role_id = prov.get_att(&sr, "RoleId").expect("RoleId should resolve");
19418 assert!(role_id.starts_with("FKIA"));
19419 }
19420
19421 #[test]
19422 fn getatt_unknown_attribute_returns_none() {
19423 let prov = make_provisioner();
19424 let bucket = make_resource(
19425 "AWS::S3::Bucket",
19426 "MyBucket",
19427 serde_json::json!({"BucketName": "my-bucket"}),
19428 );
19429 let sr = prov.create_resource(&bucket).unwrap();
19430 assert_eq!(prov.get_att(&sr, "NotARealAttr"), None);
19434 }
19435
19436 #[test]
19437 fn getatt_unknown_resource_type_returns_none() {
19438 let prov = make_provisioner();
19439 let stack_resource = StackResource {
19443 logical_id: "Mystery".to_string(),
19444 physical_id: "mystery-id".to_string(),
19445 resource_type: "AWS::Made::Up".to_string(),
19446 status: "CREATE_COMPLETE".to_string(),
19447 service_token: None,
19448 attributes: BTreeMap::new(),
19449 };
19450 assert_eq!(prov.get_att(&stack_resource, "Arn"), None);
19451 }
19452
19453 #[test]
19454 fn getatt_falls_back_to_captured_attributes() {
19455 let prov = make_provisioner();
19456 let stack_resource = StackResource {
19460 logical_id: "MyTopic".to_string(),
19461 physical_id: "arn:aws:sns:us-east-1:123456789012:my-topic".to_string(),
19462 resource_type: "AWS::SNS::Topic".to_string(),
19463 status: "CREATE_COMPLETE".to_string(),
19464 service_token: None,
19465 attributes: {
19466 let mut m = BTreeMap::new();
19467 m.insert("TopicArn".to_string(), "captured-arn".to_string());
19468 m
19469 },
19470 };
19471 assert_eq!(
19472 prov.get_att(&stack_resource, "TopicArn"),
19473 Some("captured-arn".to_string())
19474 );
19475 }
19476
19477 #[test]
19478 fn getatt_secrets_manager_arn_resolves_via_live_state() {
19479 let prov = make_provisioner();
19482 let res = make_resource(
19483 "AWS::SecretsManager::Secret",
19484 "MySecret",
19485 serde_json::json!({"Name": "my-secret", "SecretString": "hunter2"}),
19486 );
19487 let sr = prov.create_resource(&res).unwrap();
19488 let arn = prov.get_att(&sr, "Arn").expect("Arn should resolve");
19489 assert!(arn.starts_with("arn:aws:secretsmanager:"));
19490 assert!(arn.ends_with(":secret:my-secret"));
19491 }
19492
19493 #[test]
19494 fn wafv2_web_acl_lifecycle() {
19495 let prov = make_provisioner();
19496 let res = make_resource(
19497 "AWS::WAFv2::WebACL",
19498 "MyAcl",
19499 serde_json::json!({
19500 "Name": "my-acl",
19501 "Scope": "REGIONAL",
19502 "DefaultAction": {"Allow": {}},
19503 "Rules": [{"Name": "rule1", "Priority": 1, "Statement": {}, "VisibilityConfig": {}}],
19504 "VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "my-acl-metric"},
19505 "Capacity": 100,
19506 }),
19507 );
19508 let sr = prov.create_resource(&res).unwrap();
19509 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
19510 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
19511 assert_eq!(prov.get_att(&sr, "Name"), Some("my-acl".to_string()));
19512 assert!(prov.get_att(&sr, "Id").is_some());
19513 assert_eq!(prov.get_att(&sr, "Capacity"), Some("100".to_string()));
19514
19515 prov.delete_resource(&sr.clone()).unwrap();
19516 let fresh = StackResource {
19519 logical_id: "MyAcl".to_string(),
19520 physical_id: sr.physical_id.clone(),
19521 resource_type: "AWS::WAFv2::WebACL".to_string(),
19522 status: "CREATE_COMPLETE".to_string(),
19523 service_token: None,
19524 attributes: BTreeMap::new(),
19525 };
19526 assert_eq!(prov.get_att(&fresh, "Arn"), None);
19527 }
19528
19529 #[test]
19530 fn wafv2_ip_set_lifecycle() {
19531 let prov = make_provisioner();
19532 let res = make_resource(
19533 "AWS::WAFv2::IPSet",
19534 "MyIpSet",
19535 serde_json::json!({
19536 "Name": "my-ipset",
19537 "Scope": "REGIONAL",
19538 "IPAddressVersion": "IPV4",
19539 "Addresses": ["10.0.0.0/8"],
19540 }),
19541 );
19542 let sr = prov.create_resource(&res).unwrap();
19543 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
19544 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
19545 assert_eq!(prov.get_att(&sr, "Name"), Some("my-ipset".to_string()));
19546
19547 prov.delete_resource(&sr.clone()).unwrap();
19548 let fresh = StackResource {
19549 logical_id: "MyIpSet".to_string(),
19550 physical_id: sr.physical_id.clone(),
19551 resource_type: "AWS::WAFv2::IPSet".to_string(),
19552 status: "CREATE_COMPLETE".to_string(),
19553 service_token: None,
19554 attributes: BTreeMap::new(),
19555 };
19556 assert_eq!(prov.get_att(&fresh, "Arn"), None);
19557 }
19558
19559 #[test]
19560 fn wafv2_regex_pattern_set_lifecycle() {
19561 let prov = make_provisioner();
19562 let res = make_resource(
19563 "AWS::WAFv2::RegexPatternSet",
19564 "MyRegexSet",
19565 serde_json::json!({
19566 "Name": "my-regex",
19567 "Scope": "REGIONAL",
19568 "RegularExpressions": [{"RegexString": "^test"}],
19569 }),
19570 );
19571 let sr = prov.create_resource(&res).unwrap();
19572 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
19573 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
19574 assert_eq!(prov.get_att(&sr, "Name"), Some("my-regex".to_string()));
19575
19576 prov.delete_resource(&sr.clone()).unwrap();
19577 let fresh = StackResource {
19578 logical_id: "MyRegexSet".to_string(),
19579 physical_id: sr.physical_id.clone(),
19580 resource_type: "AWS::WAFv2::RegexPatternSet".to_string(),
19581 status: "CREATE_COMPLETE".to_string(),
19582 service_token: None,
19583 attributes: BTreeMap::new(),
19584 };
19585 assert_eq!(prov.get_att(&fresh, "Arn"), None);
19586 }
19587
19588 #[test]
19589 fn wafv2_rule_group_lifecycle() {
19590 let prov = make_provisioner();
19591 let res = make_resource(
19592 "AWS::WAFv2::RuleGroup",
19593 "MyRuleGroup",
19594 serde_json::json!({
19595 "Name": "my-rg",
19596 "Scope": "REGIONAL",
19597 "Capacity": 50,
19598 "Rules": [{"Name": "r1", "Priority": 1, "Statement": {}, "VisibilityConfig": {}}],
19599 "VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "rg-metric"},
19600 }),
19601 );
19602 let sr = prov.create_resource(&res).unwrap();
19603 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
19604 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
19605 assert_eq!(prov.get_att(&sr, "Name"), Some("my-rg".to_string()));
19606
19607 prov.delete_resource(&sr.clone()).unwrap();
19608 let fresh = StackResource {
19609 logical_id: "MyRuleGroup".to_string(),
19610 physical_id: sr.physical_id.clone(),
19611 resource_type: "AWS::WAFv2::RuleGroup".to_string(),
19612 status: "CREATE_COMPLETE".to_string(),
19613 service_token: None,
19614 attributes: BTreeMap::new(),
19615 };
19616 assert_eq!(prov.get_att(&fresh, "Arn"), None);
19617 }
19618
19619 #[test]
19620 fn wafv2_logging_configuration_lifecycle() {
19621 let prov = make_provisioner();
19622 let res = make_resource(
19623 "AWS::WAFv2::LoggingConfiguration",
19624 "MyLogConfig",
19625 serde_json::json!({
19626 "ResourceArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/abc",
19627 "LogDestinationConfigs": ["arn:aws:logs:us-east-1:123456789012:log-group:/aws/waf"],
19628 }),
19629 );
19630 let sr = prov.create_resource(&res).unwrap();
19631 assert_eq!(
19632 sr.physical_id,
19633 "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/abc"
19634 );
19635
19636 prov.delete_resource(&sr.clone()).unwrap();
19637 }
19638
19639 #[test]
19640 fn wafv2_web_acl_association_lifecycle() {
19641 let prov = make_provisioner();
19642 let res = make_resource(
19643 "AWS::WAFv2::WebACLAssociation",
19644 "MyAssoc",
19645 serde_json::json!({
19646 "ResourceArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188",
19647 "WebACLArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/my-acl/abc",
19648 }),
19649 );
19650 let sr = prov.create_resource(&res).unwrap();
19651 assert_eq!(sr.physical_id, "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188");
19652
19653 prov.delete_resource(&sr.clone()).unwrap();
19654 }
19655
19656 #[test]
19657 fn ses_configuration_set_lifecycle() {
19658 let prov = make_provisioner();
19659 let res = make_resource(
19660 "AWS::SES::ConfigurationSet",
19661 "MyConfigSet",
19662 serde_json::json!({
19663 "Name": "my-cs",
19664 "SendingOptions": {"SendingEnabled": true},
19665 "DeliveryOptions": {"TlsPolicy": "REQUIRE"},
19666 }),
19667 );
19668 let sr = prov.create_resource(&res).unwrap();
19669 assert_eq!(sr.physical_id, "my-cs");
19670 assert_eq!(prov.get_att(&sr, "Name"), Some("my-cs".to_string()));
19671
19672 prov.delete_resource(&sr.clone()).unwrap();
19673 let fresh = StackResource {
19674 logical_id: "MyConfigSet".to_string(),
19675 physical_id: "my-cs".to_string(),
19676 resource_type: "AWS::SES::ConfigurationSet".to_string(),
19677 status: "CREATE_COMPLETE".to_string(),
19678 service_token: None,
19679 attributes: BTreeMap::new(),
19680 };
19681 assert_eq!(prov.get_att(&fresh, "Name"), None);
19682 }
19683
19684 #[test]
19685 fn ses_email_identity_lifecycle() {
19686 let prov = make_provisioner();
19687 let res = make_resource(
19688 "AWS::SES::EmailIdentity",
19689 "MyIdentity",
19690 serde_json::json!({"EmailIdentity": "example.com"}),
19691 );
19692 let sr = prov.create_resource(&res).unwrap();
19693 assert_eq!(sr.physical_id, "example.com");
19694 assert_eq!(
19695 prov.get_att(&sr, "IdentityName"),
19696 Some("example.com".to_string())
19697 );
19698
19699 prov.delete_resource(&sr.clone()).unwrap();
19700 let fresh = StackResource {
19701 logical_id: "MyIdentity".to_string(),
19702 physical_id: "example.com".to_string(),
19703 resource_type: "AWS::SES::EmailIdentity".to_string(),
19704 status: "CREATE_COMPLETE".to_string(),
19705 service_token: None,
19706 attributes: BTreeMap::new(),
19707 };
19708 assert_eq!(prov.get_att(&fresh, "IdentityName"), None);
19709 }
19710
19711 #[test]
19712 fn ses_template_lifecycle() {
19713 let prov = make_provisioner();
19714 let res = make_resource(
19715 "AWS::SES::Template",
19716 "MyTemplate",
19717 serde_json::json!({
19718 "Template": {
19719 "TemplateName": "my-tpl",
19720 "SubjectPart": "Hello",
19721 "HtmlPart": "<h1>Hi</h1>",
19722 "TextPart": "Hi",
19723 },
19724 }),
19725 );
19726 let sr = prov.create_resource(&res).unwrap();
19727 assert_eq!(sr.physical_id, "my-tpl");
19728 assert_eq!(
19729 prov.get_att(&sr, "TemplateName"),
19730 Some("my-tpl".to_string())
19731 );
19732
19733 prov.delete_resource(&sr.clone()).unwrap();
19734 let fresh = StackResource {
19735 logical_id: "MyTemplate".to_string(),
19736 physical_id: "my-tpl".to_string(),
19737 resource_type: "AWS::SES::Template".to_string(),
19738 status: "CREATE_COMPLETE".to_string(),
19739 service_token: None,
19740 attributes: BTreeMap::new(),
19741 };
19742 assert_eq!(prov.get_att(&fresh, "TemplateName"), None);
19743 }
19744
19745 #[test]
19746 fn ses_contact_list_lifecycle() {
19747 let prov = make_provisioner();
19748 let res = make_resource(
19749 "AWS::SES::ContactList",
19750 "MyContactList",
19751 serde_json::json!({
19752 "ContactListName": "my-cl",
19753 "Description": "Test contacts",
19754 "Topics": [{"TopicName": "news", "DisplayName": "Newsletter", "Description": "Weekly news"}],
19755 }),
19756 );
19757 let sr = prov.create_resource(&res).unwrap();
19758 assert_eq!(sr.physical_id, "my-cl");
19759 assert_eq!(
19760 prov.get_att(&sr, "ContactListName"),
19761 Some("my-cl".to_string())
19762 );
19763
19764 prov.delete_resource(&sr.clone()).unwrap();
19765 let fresh = StackResource {
19766 logical_id: "MyContactList".to_string(),
19767 physical_id: "my-cl".to_string(),
19768 resource_type: "AWS::SES::ContactList".to_string(),
19769 status: "CREATE_COMPLETE".to_string(),
19770 service_token: None,
19771 attributes: BTreeMap::new(),
19772 };
19773 assert_eq!(prov.get_att(&fresh, "ContactListName"), None);
19774 }
19775
19776 #[test]
19777 fn ses_dedicated_ip_pool_lifecycle() {
19778 let prov = make_provisioner();
19779 let res = make_resource(
19780 "AWS::SES::DedicatedIpPool",
19781 "MyPool",
19782 serde_json::json!({"PoolName": "my-pool", "ScalingMode": "STANDARD"}),
19783 );
19784 let sr = prov.create_resource(&res).unwrap();
19785 assert_eq!(sr.physical_id, "my-pool");
19786 assert_eq!(prov.get_att(&sr, "PoolName"), Some("my-pool".to_string()));
19787
19788 prov.delete_resource(&sr.clone()).unwrap();
19789 let fresh = StackResource {
19790 logical_id: "MyPool".to_string(),
19791 physical_id: "my-pool".to_string(),
19792 resource_type: "AWS::SES::DedicatedIpPool".to_string(),
19793 status: "CREATE_COMPLETE".to_string(),
19794 service_token: None,
19795 attributes: BTreeMap::new(),
19796 };
19797 assert_eq!(prov.get_att(&fresh, "PoolName"), None);
19798 }
19799
19800 #[test]
19801 fn ses_receipt_rule_set_lifecycle() {
19802 let prov = make_provisioner();
19803 let res = make_resource(
19804 "AWS::SES::ReceiptRuleSet",
19805 "MyRuleSet",
19806 serde_json::json!({"RuleSetName": "my-rs"}),
19807 );
19808 let sr = prov.create_resource(&res).unwrap();
19809 assert_eq!(sr.physical_id, "my-rs");
19810 assert_eq!(prov.get_att(&sr, "RuleSetName"), Some("my-rs".to_string()));
19811
19812 prov.delete_resource(&sr.clone()).unwrap();
19813 let fresh = StackResource {
19814 logical_id: "MyRuleSet".to_string(),
19815 physical_id: "my-rs".to_string(),
19816 resource_type: "AWS::SES::ReceiptRuleSet".to_string(),
19817 status: "CREATE_COMPLETE".to_string(),
19818 service_token: None,
19819 attributes: BTreeMap::new(),
19820 };
19821 assert_eq!(prov.get_att(&fresh, "RuleSetName"), None);
19822 }
19823
19824 #[test]
19825 fn ses_receipt_rule_lifecycle() {
19826 let prov = make_provisioner();
19827 let rs = make_resource(
19828 "AWS::SES::ReceiptRuleSet",
19829 "MyRuleSet",
19830 serde_json::json!({"RuleSetName": "my-rs2"}),
19831 );
19832 prov.create_resource(&rs).unwrap();
19833
19834 let res = make_resource(
19835 "AWS::SES::ReceiptRule",
19836 "MyRule",
19837 serde_json::json!({
19838 "RuleSetName": "my-rs2",
19839 "Rule": {
19840 "Name": "rule1",
19841 "Priority": 1,
19842 "Enabled": true,
19843 "Actions": [{"S3Action": {"BucketName": "my-bucket"}}],
19844 },
19845 }),
19846 );
19847 let sr = prov.create_resource(&res).unwrap();
19848 assert_eq!(sr.physical_id, "my-rs2|rule1");
19849
19850 prov.delete_resource(&sr.clone()).unwrap();
19851 }
19852
19853 #[test]
19854 fn ses_receipt_filter_lifecycle() {
19855 let prov = make_provisioner();
19856 let res = make_resource(
19857 "AWS::SES::ReceiptFilter",
19858 "MyFilter",
19859 serde_json::json!({
19860 "Filter": {
19861 "Name": "my-filter",
19862 "IpFilter": {"Policy": "Block", "Cidr": "10.0.0.0/8"},
19863 },
19864 }),
19865 );
19866 let sr = prov.create_resource(&res).unwrap();
19867 assert_eq!(sr.physical_id, "my-filter");
19868
19869 prov.delete_resource(&sr.clone()).unwrap();
19870 }
19871
19872 #[test]
19873 fn ses_vdm_attributes_lifecycle() {
19874 let prov = make_provisioner();
19875 let res = make_resource(
19876 "AWS::SES::VdmAttributes",
19877 "MyVdm",
19878 serde_json::json!({
19879 "DashboardAttributes": {"EngagementMetrics": "ENABLED"},
19880 "GuardianAttributes": {"OptimizedSharedDelivery": "ENABLED"},
19881 }),
19882 );
19883 let sr = prov.create_resource(&res).unwrap();
19884 assert_eq!(sr.physical_id, "vdm-MyVdm");
19885
19886 prov.delete_resource(&sr.clone()).unwrap();
19887 }
19888
19889 #[test]
19890 fn athena_work_group_lifecycle() {
19891 let prov = make_provisioner();
19892 let res = make_resource(
19893 "AWS::Athena::WorkGroup",
19894 "MyWg",
19895 serde_json::json!({
19896 "Name": "my-wg",
19897 "Description": "test wg",
19898 "Configuration": {"EnforceWorkGroupConfiguration": true},
19899 }),
19900 );
19901 let sr = prov.create_resource(&res).unwrap();
19902 assert_eq!(sr.physical_id, "my-wg");
19903 assert_eq!(sr.attributes.get("Name"), Some(&"my-wg".to_string()));
19904 assert!(sr
19905 .attributes
19906 .get("Arn")
19907 .unwrap()
19908 .contains("workgroup/my-wg"));
19909
19910 assert_eq!(
19911 prov.get_att(
19912 &StackResource {
19913 resource_type: "AWS::Athena::WorkGroup".to_string(),
19914 physical_id: sr.physical_id.clone(),
19915 logical_id: "MyWg".to_string(),
19916 status: "CREATE_COMPLETE".to_string(),
19917 service_token: None,
19918 attributes: BTreeMap::new(),
19919 },
19920 "Name",
19921 ),
19922 Some("my-wg".to_string()),
19923 );
19924
19925 prov.delete_resource(&sr.clone()).unwrap();
19926 }
19927
19928 #[test]
19929 fn athena_data_catalog_lifecycle() {
19930 let prov = make_provisioner();
19931 let res = make_resource(
19932 "AWS::Athena::DataCatalog",
19933 "MyCatalog",
19934 serde_json::json!({
19935 "Name": "my-catalog",
19936 "Type": "GLUE",
19937 "Description": "test catalog",
19938 }),
19939 );
19940 let sr = prov.create_resource(&res).unwrap();
19941 assert_eq!(sr.physical_id, "my-catalog");
19942 assert_eq!(sr.attributes.get("Name"), Some(&"my-catalog".to_string()));
19943 assert!(sr
19944 .attributes
19945 .get("Arn")
19946 .unwrap()
19947 .contains("datacatalog/my-catalog"));
19948
19949 prov.delete_resource(&sr.clone()).unwrap();
19950 }
19951
19952 #[test]
19953 fn athena_named_query_lifecycle() {
19954 let prov = make_provisioner();
19955 let res = make_resource(
19956 "AWS::Athena::NamedQuery",
19957 "MyQuery",
19958 serde_json::json!({
19959 "Name": "my-query",
19960 "Database": "mydb",
19961 "QueryString": "SELECT 1",
19962 "WorkGroup": "primary",
19963 }),
19964 );
19965 let sr = prov.create_resource(&res).unwrap();
19966 assert!(!sr.physical_id.is_empty());
19967 assert_eq!(sr.attributes.get("NamedQueryId"), Some(&sr.physical_id));
19968
19969 prov.delete_resource(&sr.clone()).unwrap();
19970 }
19971
19972 #[test]
19973 fn athena_prepared_statement_lifecycle() {
19974 let prov = make_provisioner();
19975 let res = make_resource(
19976 "AWS::Athena::PreparedStatement",
19977 "MyPs",
19978 serde_json::json!({
19979 "StatementName": "my-ps",
19980 "WorkGroupName": "primary",
19981 "QueryStatement": "SELECT 1",
19982 }),
19983 );
19984 let sr = prov.create_resource(&res).unwrap();
19985 assert_eq!(sr.physical_id, "primary|my-ps");
19986
19987 prov.delete_resource(&sr.clone()).unwrap();
19988 }
19989}