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 durable_config: None,
3895 signing_profile_version_arn: None,
3896 signing_job_arn: None,
3897 runtime_version_config: None,
3898 master_arn: None,
3899 state_reason: None,
3900 state_reason_code: None,
3901 last_update_status_reason: None,
3902 last_update_status_reason_code: None,
3903 };
3904
3905 let mut accounts = self.lambda_state.write();
3906 let state = accounts.get_or_create(&self.account_id);
3907 state.functions.insert(function_name.clone(), func);
3908
3909 Ok(ProvisionResult::new(function_name.clone())
3914 .with("Arn", function_arn)
3915 .with("FunctionName", function_name)
3916 .with("Version", "$LATEST"))
3917 }
3918
3919 fn update_lambda_function(
3927 &self,
3928 existing: &StackResource,
3929 resource: &ResourceDefinition,
3930 ) -> Result<ProvisionResult, String> {
3931 let props = &resource.properties;
3932 let function_name = existing.physical_id.clone();
3933 let cfg = parse_lambda_function_props(props)?;
3934
3935 let new_code_zip = if cfg.code_zip.is_some() {
3936 cfg.code_zip.clone()
3937 } else if let (Some(bucket_name), Some(key)) = (&cfg.s3_bucket, &cfg.s3_key) {
3938 Some(self.read_s3_object_bytes(bucket_name, key).map_err(|e| {
3939 format!("Failed to read Code.S3Bucket={bucket_name} Code.S3Key={key}: {e}")
3940 })?)
3941 } else {
3942 None
3943 };
3944
3945 let resolved_layers: Vec<AttachedLayer> = {
3946 let accounts = self.lambda_state.read();
3947 cfg.layers
3948 .iter()
3949 .map(|arn| AttachedLayer {
3950 arn: arn.clone(),
3951 code_size: layer_code_size(&accounts, arn),
3952 })
3953 .collect()
3954 };
3955
3956 let mut accounts = self.lambda_state.write();
3957 let state = accounts.get_or_create(&self.account_id);
3958 let func = state.functions.get_mut(&function_name).ok_or_else(|| {
3959 format!("Cannot update {function_name}: function does not exist in lambda state")
3960 })?;
3961
3962 func.runtime = cfg.runtime;
3963 func.role = cfg.role;
3964 func.handler = cfg.handler;
3965 func.description = cfg.description;
3966 func.timeout = cfg.timeout;
3967 func.memory_size = cfg.memory_size;
3968 func.environment = cfg.environment;
3969 func.architectures = cfg.architectures;
3970 func.package_type = cfg.package_type;
3971 func.tracing_mode = cfg.tracing_mode;
3972 func.kms_key_arn = cfg.kms_key_arn;
3973 func.ephemeral_storage_size = cfg.ephemeral_storage_size;
3974 func.vpc_config = cfg.vpc_config;
3975 func.snap_start = cfg.snap_start;
3976 func.dead_letter_config_arn = cfg.dead_letter_config_arn;
3977 func.file_system_configs = cfg.file_system_configs;
3978 func.logging_config = cfg.logging_config;
3979 func.layers = resolved_layers;
3980 if cfg.image_uri.is_some() {
3981 func.image_uri = cfg.image_uri;
3982 }
3983 if !cfg.tags.is_empty() {
3984 func.tags = cfg.tags;
3985 }
3986 if let Some(bytes) = new_code_zip {
3987 func.code_sha256 = sha256_b64(&bytes);
3988 func.code_size = bytes.len() as i64;
3989 func.code_zip = Some(bytes);
3990 }
3991 func.last_modified = Utc::now();
3992 func.revision_id = Uuid::new_v4().to_string();
3993
3994 let function_arn = func.function_arn.clone();
3995 Ok(ProvisionResult::new(function_name.clone())
3996 .with("Arn", function_arn)
3997 .with("FunctionName", function_name)
3998 .with("Version", "$LATEST"))
3999 }
4000
4001 fn delete_lambda_function(&self, physical_id: &str) -> Result<(), String> {
4002 let mut accounts = self.lambda_state.write();
4003 let state = accounts.default_mut();
4004 state.functions.remove(physical_id);
4005 Ok(())
4006 }
4007
4008 fn read_s3_object_bytes(&self, bucket: &str, key: &str) -> Result<Vec<u8>, String> {
4014 let mut accounts = self.s3_state.write();
4015 let state = accounts.get_or_create(&self.account_id);
4016 let body_ref = {
4017 let b = state
4018 .buckets
4019 .get(bucket)
4020 .ok_or_else(|| format!("S3 bucket {bucket} does not exist"))?;
4021 let object = b
4022 .objects
4023 .get(key)
4024 .ok_or_else(|| format!("S3 object s3://{bucket}/{key} does not exist"))?;
4025 object.body.clone()
4026 };
4027 state
4030 .read_body(&body_ref)
4031 .map(|b| b.to_vec())
4032 .map_err(|e| format!("S3 read failed: {e}"))
4033 }
4034
4035 fn create_lambda_permission(
4036 &self,
4037 resource: &ResourceDefinition,
4038 ) -> Result<ProvisionResult, String> {
4039 let props = &resource.properties;
4040 let function_name = parse_lambda_function_name(
4041 props
4042 .get("FunctionName")
4043 .and_then(|v| v.as_str())
4044 .ok_or_else(|| "FunctionName is required".to_string())?,
4045 );
4046 let statement_id = format!(
4050 "cfn-{}-{}",
4051 resource.logical_id,
4052 &Uuid::new_v4().simple().to_string()[..8]
4053 );
4054 self.append_lambda_permission_statement(&function_name, &statement_id, props)?;
4055
4056 let physical_id = format!("{function_name}|{statement_id}");
4058 Ok(ProvisionResult::new(physical_id).with("Id", statement_id))
4059 }
4060
4061 fn append_lambda_permission_statement(
4067 &self,
4068 function_name: &str,
4069 statement_id: &str,
4070 props: &serde_json::Value,
4071 ) -> Result<String, String> {
4072 let action = props
4073 .get("Action")
4074 .and_then(|v| v.as_str())
4075 .ok_or_else(|| "Action is required".to_string())?
4076 .to_string();
4077 let principal = props
4078 .get("Principal")
4079 .and_then(|v| v.as_str())
4080 .ok_or_else(|| "Principal is required".to_string())?
4081 .to_string();
4082 let source_arn = props
4083 .get("SourceArn")
4084 .and_then(|v| v.as_str())
4085 .map(|s| s.to_string());
4086 let source_account = props
4087 .get("SourceAccount")
4088 .and_then(|v| v.as_str())
4089 .map(|s| s.to_string());
4090 let event_source_token = props
4091 .get("EventSourceToken")
4092 .and_then(|v| v.as_str())
4093 .map(|s| s.to_string());
4094 let function_url_auth_type = props
4095 .get("FunctionUrlAuthType")
4096 .and_then(|v| v.as_str())
4097 .map(|s| s.to_string());
4098 let principal_org_id = props
4099 .get("PrincipalOrgID")
4100 .and_then(|v| v.as_str())
4101 .map(|s| s.to_string());
4102
4103 let mut accounts = self.lambda_state.write();
4104 let state = accounts.get_or_create(&self.account_id);
4105 let func = state.functions.get_mut(function_name).ok_or_else(|| {
4106 format!(
4107 "Function {function_name} does not exist yet — retry once it has been provisioned"
4108 )
4109 })?;
4110
4111 let mut doc: serde_json::Value = func
4112 .policy
4113 .as_deref()
4114 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
4115 .filter(|v| v.is_object())
4116 .unwrap_or_else(|| serde_json::json!({"Version": "2012-10-17", "Statement": []}));
4117 if !doc.get("Statement").map(|s| s.is_array()).unwrap_or(false) {
4118 doc["Statement"] = serde_json::json!([]);
4119 }
4120 let principal_value =
4121 if principal.ends_with(".amazonaws.com") || principal.contains(".amazon") {
4122 serde_json::json!({ "Service": principal })
4123 } else {
4124 serde_json::json!({ "AWS": principal })
4125 };
4126 let mut arn_like = serde_json::Map::new();
4127 let mut string_equals = serde_json::Map::new();
4128 if let Some(src) = source_arn {
4129 arn_like.insert("AWS:SourceArn".to_string(), serde_json::Value::String(src));
4130 }
4131 if let Some(acct) = source_account {
4132 string_equals.insert(
4133 "AWS:SourceAccount".to_string(),
4134 serde_json::Value::String(acct),
4135 );
4136 }
4137 if let Some(token) = event_source_token {
4138 string_equals.insert(
4139 "lambda:EventSourceToken".to_string(),
4140 serde_json::Value::String(token),
4141 );
4142 }
4143 if let Some(auth) = function_url_auth_type {
4144 string_equals.insert(
4145 "lambda:FunctionUrlAuthType".to_string(),
4146 serde_json::Value::String(auth),
4147 );
4148 }
4149 if let Some(org) = principal_org_id {
4150 string_equals.insert(
4151 "aws:PrincipalOrgID".to_string(),
4152 serde_json::Value::String(org),
4153 );
4154 }
4155 let mut conditions = serde_json::Map::new();
4156 if !arn_like.is_empty() {
4157 conditions.insert("ArnLike".to_string(), serde_json::Value::Object(arn_like));
4158 }
4159 if !string_equals.is_empty() {
4160 conditions.insert(
4161 "StringEquals".to_string(),
4162 serde_json::Value::Object(string_equals),
4163 );
4164 }
4165
4166 let mut statement = serde_json::Map::new();
4167 statement.insert(
4168 "Sid".to_string(),
4169 serde_json::Value::String(statement_id.to_string()),
4170 );
4171 statement.insert(
4172 "Effect".to_string(),
4173 serde_json::Value::String("Allow".to_string()),
4174 );
4175 statement.insert("Principal".to_string(), principal_value);
4176 statement.insert("Action".to_string(), serde_json::Value::String(action));
4177 statement.insert(
4178 "Resource".to_string(),
4179 serde_json::Value::String(func.function_arn.clone()),
4180 );
4181 if !conditions.is_empty() {
4182 statement.insert(
4183 "Condition".to_string(),
4184 serde_json::Value::Object(conditions),
4185 );
4186 }
4187 doc["Statement"]
4188 .as_array_mut()
4189 .unwrap()
4190 .push(serde_json::Value::Object(statement));
4191 func.policy = Some(doc.to_string());
4192 Ok(func.function_arn.clone())
4193 }
4194
4195 fn update_lambda_permission(
4200 &self,
4201 existing: &StackResource,
4202 resource: &ResourceDefinition,
4203 ) -> Result<ProvisionResult, String> {
4204 let Some((function_name, statement_id)) = existing.physical_id.split_once('|') else {
4205 return Err(format!(
4206 "Permission physical id `{}` is malformed; expected `{{function}}|{{sid}}`",
4207 existing.physical_id
4208 ));
4209 };
4210 {
4213 let mut accounts = self.lambda_state.write();
4214 let state = accounts.get_or_create(&self.account_id);
4215 if let Some(func) = state.functions.get_mut(function_name) {
4216 if let Some(policy_str) = func.policy.as_deref() {
4217 if let Ok(mut doc) = serde_json::from_str::<serde_json::Value>(policy_str) {
4218 if let Some(arr) = doc.get_mut("Statement").and_then(|v| v.as_array_mut()) {
4219 arr.retain(|s| {
4220 s.get("Sid").and_then(|v| v.as_str()) != Some(statement_id)
4221 });
4222 func.policy = Some(doc.to_string());
4223 }
4224 }
4225 }
4226 }
4227 }
4228 self.append_lambda_permission_statement(function_name, statement_id, &resource.properties)?;
4229 Ok(ProvisionResult::new(existing.physical_id.clone()).with("Id", statement_id.to_string()))
4230 }
4231
4232 fn delete_lambda_permission(&self, physical_id: &str) -> Result<(), String> {
4233 let Some((function_name, sid)) = physical_id.split_once('|') else {
4234 return Ok(());
4235 };
4236 let mut accounts = self.lambda_state.write();
4237 let state = accounts.get_or_create(&self.account_id);
4238 if let Some(func) = state.functions.get_mut(function_name) {
4239 if let Some(policy_str) = func.policy.as_deref() {
4240 if let Ok(mut doc) = serde_json::from_str::<serde_json::Value>(policy_str) {
4241 if let Some(arr) = doc.get_mut("Statement").and_then(|v| v.as_array_mut()) {
4242 arr.retain(|s| s.get("Sid").and_then(|v| v.as_str()) != Some(sid));
4243 func.policy = Some(doc.to_string());
4244 }
4245 }
4246 }
4247 }
4248 Ok(())
4249 }
4250
4251 fn create_lambda_event_source_mapping(
4252 &self,
4253 resource: &ResourceDefinition,
4254 ) -> Result<ProvisionResult, String> {
4255 let props = &resource.properties;
4256 let function_name = parse_lambda_function_name(
4257 props
4258 .get("FunctionName")
4259 .and_then(|v| v.as_str())
4260 .ok_or_else(|| "FunctionName is required".to_string())?,
4261 );
4262 let cfg = parse_lambda_event_source_mapping_props(props)?;
4263
4264 let mut accounts = self.lambda_state.write();
4265 let state = accounts.get_or_create(&self.account_id);
4266 if !state.functions.contains_key(&function_name) {
4267 return Err(format!(
4268 "Function {function_name} does not exist yet — retry once it has been provisioned"
4269 ));
4270 }
4271 let function_arn = format!(
4272 "arn:aws:lambda:{}:{}:function:{}",
4273 self.region, self.account_id, function_name
4274 );
4275 let uuid = Uuid::new_v4().to_string();
4276 let esm = EventSourceMapping {
4277 uuid: uuid.clone(),
4278 function_arn,
4279 event_source_arn: cfg.event_source_arn,
4280 batch_size: cfg.batch_size,
4281 enabled: cfg.enabled,
4282 state: if cfg.enabled {
4283 "Enabled".to_string()
4284 } else {
4285 "Disabled".to_string()
4286 },
4287 last_modified: Utc::now(),
4288 filter_patterns: cfg.filter_patterns,
4289 maximum_batching_window_in_seconds: cfg.maximum_batching_window_in_seconds,
4290 starting_position: cfg.starting_position,
4291 starting_position_timestamp: cfg.starting_position_timestamp,
4292 parallelization_factor: cfg.parallelization_factor,
4293 function_response_types: cfg.function_response_types,
4294 kms_key_arn: cfg.kms_key_arn,
4295 metrics_config: cfg.metrics_config,
4296 destination_config: cfg.destination_config,
4297 maximum_retry_attempts: cfg.maximum_retry_attempts,
4298 maximum_record_age_in_seconds: cfg.maximum_record_age_in_seconds,
4299 bisect_batch_on_function_error: cfg.bisect_batch_on_function_error,
4300 tumbling_window_in_seconds: cfg.tumbling_window_in_seconds,
4301 topics: cfg.topics,
4302 queues: cfg.queues,
4303 };
4304 state.event_source_mappings.insert(uuid.clone(), esm);
4305 Ok(ProvisionResult::new(uuid.clone()).with("Id", uuid))
4306 }
4307
4308 fn update_lambda_event_source_mapping(
4312 &self,
4313 existing: &StackResource,
4314 resource: &ResourceDefinition,
4315 ) -> Result<ProvisionResult, String> {
4316 let cfg = parse_lambda_event_source_mapping_props(&resource.properties)?;
4317 let mut accounts = self.lambda_state.write();
4318 let state = accounts.get_or_create(&self.account_id);
4319 let esm = state
4320 .event_source_mappings
4321 .get_mut(&existing.physical_id)
4322 .ok_or_else(|| {
4323 format!(
4324 "EventSourceMapping {} does not exist in lambda state",
4325 existing.physical_id
4326 )
4327 })?;
4328 esm.batch_size = cfg.batch_size;
4329 esm.enabled = cfg.enabled;
4330 esm.state = if cfg.enabled {
4331 "Enabled".to_string()
4332 } else {
4333 "Disabled".to_string()
4334 };
4335 esm.last_modified = Utc::now();
4336 esm.filter_patterns = cfg.filter_patterns;
4337 esm.maximum_batching_window_in_seconds = cfg.maximum_batching_window_in_seconds;
4338 esm.parallelization_factor = cfg.parallelization_factor;
4339 esm.function_response_types = cfg.function_response_types;
4340 esm.kms_key_arn = cfg.kms_key_arn;
4341 esm.metrics_config = cfg.metrics_config;
4342 esm.destination_config = cfg.destination_config;
4343 esm.maximum_retry_attempts = cfg.maximum_retry_attempts;
4344 esm.maximum_record_age_in_seconds = cfg.maximum_record_age_in_seconds;
4345 esm.bisect_batch_on_function_error = cfg.bisect_batch_on_function_error;
4346 esm.tumbling_window_in_seconds = cfg.tumbling_window_in_seconds;
4347 Ok(ProvisionResult::new(existing.physical_id.clone())
4348 .with("Id", existing.physical_id.clone()))
4349 }
4350
4351 fn delete_lambda_event_source_mapping(&self, physical_id: &str) -> Result<(), String> {
4352 let mut accounts = self.lambda_state.write();
4353 let state = accounts.get_or_create(&self.account_id);
4354 state.event_source_mappings.remove(physical_id);
4355 Ok(())
4356 }
4357
4358 fn create_lambda_layer_version(
4359 &self,
4360 resource: &ResourceDefinition,
4361 ) -> Result<ProvisionResult, String> {
4362 let props = &resource.properties;
4363 let layer_name = props
4364 .get("LayerName")
4365 .and_then(|v| v.as_str())
4366 .unwrap_or(&resource.logical_id)
4367 .to_string();
4368 let description = props
4369 .get("Description")
4370 .and_then(|v| v.as_str())
4371 .unwrap_or("")
4372 .to_string();
4373 let license_info = props
4374 .get("LicenseInfo")
4375 .and_then(|v| v.as_str())
4376 .unwrap_or("")
4377 .to_string();
4378 let compatible_runtimes: Vec<String> = props
4379 .get("CompatibleRuntimes")
4380 .and_then(|v| v.as_array())
4381 .map(|arr| {
4382 arr.iter()
4383 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4384 .collect()
4385 })
4386 .unwrap_or_default();
4387 let compatible_architectures: Vec<String> = props
4388 .get("CompatibleArchitectures")
4389 .and_then(|v| v.as_array())
4390 .map(|arr| {
4391 arr.iter()
4392 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4393 .collect()
4394 })
4395 .unwrap_or_default();
4396 let content = props.get("Content");
4403 let zip_bytes = if let Some(b64) = content
4404 .and_then(|v| v.get("ZipFile"))
4405 .and_then(|v| v.as_str())
4406 {
4407 use base64::Engine;
4408 Some(
4409 base64::engine::general_purpose::STANDARD
4410 .decode(b64)
4411 .map_err(|e| format!("Content.ZipFile is not valid base64: {e}"))?,
4412 )
4413 } else if let (Some(bucket), Some(key)) = (
4414 content
4415 .and_then(|c| c.get("S3Bucket"))
4416 .and_then(|v| v.as_str()),
4417 content
4418 .and_then(|c| c.get("S3Key"))
4419 .and_then(|v| v.as_str()),
4420 ) {
4421 Some(self.read_s3_object_bytes(bucket, key).map_err(|e| {
4422 format!("Failed to read Content.S3Bucket={bucket} Content.S3Key={key}: {e}")
4423 })?)
4424 } else {
4425 None
4426 };
4427
4428 let (code_sha256, code_size) = match zip_bytes.as_deref() {
4429 Some(bytes) => (sha256_b64(bytes), bytes.len() as i64),
4430 None => (String::new(), 0),
4431 };
4432
4433 let mut accounts = self.lambda_state.write();
4434 let state = accounts.get_or_create(&self.account_id);
4435 let layer_arn = format!(
4436 "arn:aws:lambda:{}:{}:layer:{}",
4437 self.region, self.account_id, layer_name
4438 );
4439 let layer = state
4440 .layers
4441 .entry(layer_name.clone())
4442 .or_insert_with(|| Layer {
4443 layer_name: layer_name.clone(),
4444 layer_arn: layer_arn.clone(),
4445 versions: Vec::new(),
4446 });
4447 let next_version = (layer.versions.len() as i64) + 1;
4448 let version_arn = format!("{}:{}", layer.layer_arn, next_version);
4449 layer.versions.push(LayerVersion {
4450 version: next_version,
4451 layer_version_arn: version_arn.clone(),
4452 description: description.clone(),
4453 created_date: Utc::now(),
4454 compatible_runtimes,
4455 license_info,
4456 policy: None,
4457 code_zip: zip_bytes,
4458 code_sha256,
4459 code_size,
4460 compatible_architectures,
4461 });
4462 Ok(ProvisionResult::new(version_arn.clone())
4463 .with("LayerVersionArn", version_arn)
4464 .with("LayerArn", layer_arn))
4465 }
4466
4467 fn delete_lambda_layer_version(&self, physical_id: &str) -> Result<(), String> {
4468 let Some(idx) = physical_id.rfind(':') else {
4470 return Ok(());
4471 };
4472 let (layer_arn, version_part) = physical_id.split_at(idx);
4473 let version_part = &version_part[1..];
4474 let Ok(version) = version_part.parse::<i64>() else {
4475 return Ok(());
4476 };
4477 let layer_name = layer_arn.rsplit(':').next().unwrap_or("").to_string();
4479 let mut accounts = self.lambda_state.write();
4480 let state = accounts.get_or_create(&self.account_id);
4481 if let Some(layer) = state.layers.get_mut(&layer_name) {
4482 layer.versions.retain(|v| v.version != version);
4483 if layer.versions.is_empty() {
4484 state.layers.remove(&layer_name);
4485 }
4486 }
4487 Ok(())
4488 }
4489
4490 fn create_lambda_url(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
4491 let props = &resource.properties;
4492 let function_name = parse_lambda_function_name(
4493 props
4494 .get("TargetFunctionArn")
4495 .and_then(|v| v.as_str())
4496 .ok_or_else(|| "TargetFunctionArn is required".to_string())?,
4497 );
4498 let qualifier = props
4503 .get("Qualifier")
4504 .and_then(|v| v.as_str())
4505 .filter(|s| !s.is_empty())
4506 .map(|s| s.to_string());
4507 let auth_type = props
4508 .get("AuthType")
4509 .and_then(|v| v.as_str())
4510 .unwrap_or("NONE")
4511 .to_string();
4512 let invoke_mode = props
4513 .get("InvokeMode")
4514 .and_then(|v| v.as_str())
4515 .unwrap_or("BUFFERED")
4516 .to_string();
4517 let cors = props.get("Cors").cloned();
4518
4519 let mut accounts = self.lambda_state.write();
4520 let state = accounts.get_or_create(&self.account_id);
4521 if !state.functions.contains_key(&function_name) {
4522 return Err(format!(
4523 "Function {function_name} does not exist yet — retry once it has been provisioned"
4524 ));
4525 }
4526 let function_arn = match &qualifier {
4527 Some(q) => format!(
4528 "arn:aws:lambda:{}:{}:function:{}:{}",
4529 self.region, self.account_id, function_name, q
4530 ),
4531 None => format!(
4532 "arn:aws:lambda:{}:{}:function:{}",
4533 self.region, self.account_id, function_name
4534 ),
4535 };
4536 let function_url = format!("https://{function_name}.lambda-url.{}.on.aws/", self.region);
4537 let now = Utc::now();
4538 let cfg = FunctionUrlConfig {
4539 function_arn: function_arn.clone(),
4540 function_url: function_url.clone(),
4541 auth_type,
4542 cors,
4543 creation_time: now,
4544 last_modified_time: now,
4545 invoke_mode,
4546 };
4547 let key = match &qualifier {
4551 Some(q) => format!("{function_name}:{q}"),
4552 None => function_name.clone(),
4553 };
4554 state.function_url_configs.insert(key.clone(), cfg);
4555
4556 Ok(ProvisionResult::new(key)
4557 .with("FunctionArn", function_arn)
4558 .with("FunctionUrl", function_url))
4559 }
4560
4561 fn update_lambda_url(
4566 &self,
4567 existing: &StackResource,
4568 resource: &ResourceDefinition,
4569 ) -> Result<ProvisionResult, String> {
4570 let props = &resource.properties;
4571 let auth_type = props
4572 .get("AuthType")
4573 .and_then(|v| v.as_str())
4574 .unwrap_or("NONE")
4575 .to_string();
4576 let invoke_mode = props
4577 .get("InvokeMode")
4578 .and_then(|v| v.as_str())
4579 .unwrap_or("BUFFERED")
4580 .to_string();
4581 let cors = props.get("Cors").cloned();
4582
4583 let mut accounts = self.lambda_state.write();
4584 let state = accounts.get_or_create(&self.account_id);
4585 let cfg = state
4586 .function_url_configs
4587 .get_mut(&existing.physical_id)
4588 .ok_or_else(|| {
4589 format!(
4590 "FunctionUrlConfig {} does not exist in lambda state",
4591 existing.physical_id
4592 )
4593 })?;
4594 cfg.auth_type = auth_type;
4595 cfg.invoke_mode = invoke_mode;
4596 cfg.cors = cors;
4597 cfg.last_modified_time = Utc::now();
4598 let function_arn = cfg.function_arn.clone();
4599 let function_url = cfg.function_url.clone();
4600 Ok(ProvisionResult::new(existing.physical_id.clone())
4601 .with("FunctionArn", function_arn)
4602 .with("FunctionUrl", function_url))
4603 }
4604
4605 fn delete_lambda_url(&self, physical_id: &str) -> Result<(), String> {
4606 let mut accounts = self.lambda_state.write();
4607 let state = accounts.get_or_create(&self.account_id);
4608 state.function_url_configs.remove(physical_id);
4609 Ok(())
4610 }
4611
4612 fn create_lambda_alias(
4613 &self,
4614 resource: &ResourceDefinition,
4615 ) -> Result<ProvisionResult, String> {
4616 let props = &resource.properties;
4617 let function_name = parse_lambda_function_name(
4618 props
4619 .get("FunctionName")
4620 .and_then(|v| v.as_str())
4621 .ok_or_else(|| "FunctionName is required".to_string())?,
4622 );
4623 let alias_name = props
4624 .get("Name")
4625 .and_then(|v| v.as_str())
4626 .ok_or_else(|| "Name is required".to_string())?
4627 .to_string();
4628 let function_version = props
4629 .get("FunctionVersion")
4630 .and_then(|v| v.as_str())
4631 .unwrap_or("$LATEST")
4632 .to_string();
4633 let description = props
4634 .get("Description")
4635 .and_then(|v| v.as_str())
4636 .unwrap_or("")
4637 .to_string();
4638 let routing_config = props.get("RoutingConfig").cloned();
4639
4640 let mut accounts = self.lambda_state.write();
4641 let state = accounts.get_or_create(&self.account_id);
4642 if !state.functions.contains_key(&function_name) {
4643 return Err(format!(
4644 "Function {function_name} does not exist yet — retry once it has been provisioned"
4645 ));
4646 }
4647 let alias_arn = format!(
4648 "arn:aws:lambda:{}:{}:function:{}:{}",
4649 self.region, self.account_id, function_name, alias_name
4650 );
4651 let key = format!("{function_name}:{alias_name}");
4652 state.aliases.insert(
4653 key.clone(),
4654 FunctionAlias {
4655 alias_arn: alias_arn.clone(),
4656 name: alias_name,
4657 function_version,
4658 description,
4659 revision_id: Uuid::new_v4().to_string(),
4660 routing_config,
4661 },
4662 );
4663 if let Some(cnt) = props
4669 .get("ProvisionedConcurrencyConfig")
4670 .and_then(|v| v.get("ProvisionedConcurrentExecutions"))
4671 .and_then(|v| v.as_i64())
4672 {
4673 state.provisioned_concurrency.insert(
4674 key.clone(),
4675 fakecloud_lambda::ProvisionedConcurrencyConfig {
4676 requested: cnt,
4677 allocated: cnt,
4678 status: "READY".to_string(),
4679 last_modified: Utc::now(),
4680 },
4681 );
4682 }
4683 Ok(ProvisionResult::new(key).with("AliasArn", alias_arn))
4684 }
4685
4686 fn update_lambda_alias(
4692 &self,
4693 existing: &StackResource,
4694 resource: &ResourceDefinition,
4695 ) -> Result<ProvisionResult, String> {
4696 let props = &resource.properties;
4697 let function_version = props
4698 .get("FunctionVersion")
4699 .and_then(|v| v.as_str())
4700 .unwrap_or("$LATEST")
4701 .to_string();
4702 let description = props
4703 .get("Description")
4704 .and_then(|v| v.as_str())
4705 .unwrap_or("")
4706 .to_string();
4707 let routing_config = props.get("RoutingConfig").cloned();
4708
4709 let mut accounts = self.lambda_state.write();
4710 let state = accounts.get_or_create(&self.account_id);
4711 let alias = state
4712 .aliases
4713 .get_mut(&existing.physical_id)
4714 .ok_or_else(|| {
4715 format!(
4716 "Alias {} does not exist in lambda state",
4717 existing.physical_id
4718 )
4719 })?;
4720 alias.function_version = function_version;
4721 alias.description = description;
4722 alias.routing_config = routing_config;
4723 alias.revision_id = Uuid::new_v4().to_string();
4724 let alias_arn = alias.alias_arn.clone();
4725
4726 match props
4729 .get("ProvisionedConcurrencyConfig")
4730 .and_then(|v| v.get("ProvisionedConcurrentExecutions"))
4731 .and_then(|v| v.as_i64())
4732 {
4733 Some(cnt) => {
4734 state.provisioned_concurrency.insert(
4735 existing.physical_id.clone(),
4736 fakecloud_lambda::ProvisionedConcurrencyConfig {
4737 requested: cnt,
4738 allocated: cnt,
4739 status: "READY".to_string(),
4740 last_modified: Utc::now(),
4741 },
4742 );
4743 }
4744 None => {
4745 state.provisioned_concurrency.remove(&existing.physical_id);
4746 }
4747 }
4748 Ok(ProvisionResult::new(existing.physical_id.clone()).with("AliasArn", alias_arn))
4749 }
4750
4751 fn delete_lambda_alias(&self, physical_id: &str) -> Result<(), String> {
4752 let mut accounts = self.lambda_state.write();
4753 let state = accounts.get_or_create(&self.account_id);
4754 state.aliases.remove(physical_id);
4755 Ok(())
4756 }
4757
4758 fn create_lambda_version(
4759 &self,
4760 resource: &ResourceDefinition,
4761 ) -> Result<ProvisionResult, String> {
4762 let props = &resource.properties;
4763 let function_name = parse_lambda_function_name(
4764 props
4765 .get("FunctionName")
4766 .and_then(|v| v.as_str())
4767 .ok_or_else(|| "FunctionName is required".to_string())?,
4768 );
4769 let description_override = props
4774 .get("Description")
4775 .and_then(|v| v.as_str())
4776 .map(|s| s.to_string());
4777 let expected_sha = props
4778 .get("CodeSha256")
4779 .and_then(|v| v.as_str())
4780 .map(|s| s.to_string());
4781
4782 let mut accounts = self.lambda_state.write();
4783 let state = accounts.get_or_create(&self.account_id);
4784 let func = state
4785 .functions
4786 .get(&function_name)
4787 .ok_or_else(|| format!("Function {function_name} does not exist yet — retry once it has been provisioned"))?
4788 .clone();
4789 if let Some(expected) = &expected_sha {
4790 if !expected.is_empty() && expected != &func.code_sha256 {
4791 return Err(format!(
4792 "PreconditionFailed: CodeSha256 mismatch on {function_name} — expected {expected}, $LATEST is {actual}",
4793 actual = func.code_sha256,
4794 ));
4795 }
4796 }
4797 let versions = state
4798 .function_versions
4799 .entry(function_name.clone())
4800 .or_default();
4801 let next_version = (versions.len() as i64 + 1).to_string();
4802 versions.push(next_version.clone());
4803 let mut snapshot = func.clone();
4806 snapshot.version = next_version.clone();
4807 if let Some(desc) = description_override {
4808 snapshot.description = desc;
4809 }
4810 state
4811 .function_version_snapshots
4812 .entry(function_name.clone())
4813 .or_default()
4814 .insert(next_version.clone(), snapshot);
4815 let version_arn = format!(
4816 "arn:aws:lambda:{}:{}:function:{}:{}",
4817 self.region, self.account_id, function_name, next_version
4818 );
4819 let physical_id = format!("{function_name}:{next_version}");
4820 Ok(ProvisionResult::new(physical_id)
4821 .with("Version", next_version)
4822 .with("FunctionArn", version_arn))
4823 }
4824
4825 fn update_lambda_version(
4831 &self,
4832 existing: &StackResource,
4833 _resource: &ResourceDefinition,
4834 ) -> Result<ProvisionResult, String> {
4835 let mut accounts = self.lambda_state.write();
4836 let state = accounts.get_or_create(&self.account_id);
4837 let Some((function_name, version)) = existing.physical_id.split_once(':') else {
4838 return Err(format!(
4839 "Version physical id `{}` is malformed; expected `{{function}}:{{version}}`",
4840 existing.physical_id
4841 ));
4842 };
4843 let exists = state
4844 .function_version_snapshots
4845 .get(function_name)
4846 .map(|m| m.contains_key(version))
4847 .unwrap_or(false);
4848 if !exists {
4849 return Err(format!(
4850 "Version {version} for function {function_name} no longer exists in lambda state"
4851 ));
4852 }
4853 let version_arn = format!(
4854 "arn:aws:lambda:{}:{}:function:{}:{}",
4855 self.region, self.account_id, function_name, version
4856 );
4857 Ok(ProvisionResult::new(existing.physical_id.clone())
4858 .with("Version", version.to_string())
4859 .with("FunctionArn", version_arn))
4860 }
4861
4862 fn update_lambda_layer_version(
4868 &self,
4869 existing: &StackResource,
4870 _resource: &ResourceDefinition,
4871 ) -> Result<ProvisionResult, String> {
4872 let arn = existing.physical_id.clone();
4873 let layer_arn_only = arn
4875 .rsplit_once(':')
4876 .map(|(prefix, _)| prefix.to_string())
4877 .unwrap_or_else(|| arn.clone());
4878 Ok(ProvisionResult::new(existing.physical_id.clone())
4879 .with("LayerVersionArn", arn)
4880 .with("LayerArn", layer_arn_only))
4881 }
4882
4883 fn delete_lambda_version(&self, physical_id: &str) -> Result<(), String> {
4884 let Some((function_name, version)) = physical_id.split_once(':') else {
4885 return Ok(());
4886 };
4887 let mut accounts = self.lambda_state.write();
4888 let state = accounts.get_or_create(&self.account_id);
4889 if let Some(versions) = state.function_versions.get_mut(function_name) {
4890 versions.retain(|v| v != version);
4891 }
4892 if let Some(snapshots) = state.function_version_snapshots.get_mut(function_name) {
4893 snapshots.remove(version);
4894 }
4895 Ok(())
4896 }
4897
4898 fn create_secrets_manager_secret(
4901 &self,
4902 resource: &ResourceDefinition,
4903 ) -> Result<ProvisionResult, String> {
4904 let props = &resource.properties;
4905 let name = props
4906 .get("Name")
4907 .and_then(|v| v.as_str())
4908 .unwrap_or(&resource.logical_id)
4909 .to_string();
4910 let description = props
4911 .get("Description")
4912 .and_then(|v| v.as_str())
4913 .map(|s| s.to_string());
4914 let kms_key_id = props
4915 .get("KmsKeyId")
4916 .and_then(|v| v.as_str())
4917 .map(|s| s.to_string());
4918
4919 let mut accounts = self.secretsmanager_state.write();
4920 let state = accounts.get_or_create(&self.account_id);
4921 let arn = format!(
4922 "arn:aws:secretsmanager:{}:{}:secret:{}",
4923 state.region, state.account_id, name
4924 );
4925
4926 if state.secrets.contains_key(&arn) {
4927 return Err(format!("Secret {name} already exists"));
4928 }
4929
4930 let now = Utc::now();
4931 let mut versions = BTreeMap::new();
4932 let mut current_version_id: Option<String> = None;
4933 let initial_string: Option<String> =
4934 if let Some(secret_string) = props.get("SecretString").and_then(|v| v.as_str()) {
4935 Some(secret_string.to_string())
4936 } else if let Some(gen) = props.get("GenerateSecretString") {
4937 Some(generate_secret_string_payload(gen)?)
4938 } else {
4939 None
4940 };
4941 if let Some(secret_string) = initial_string {
4942 let version_id = Uuid::new_v4().to_string();
4943 versions.insert(
4944 version_id.clone(),
4945 SecretVersion {
4946 version_id: version_id.clone(),
4947 secret_string: Some(secret_string),
4948 secret_binary: None,
4949 stages: vec!["AWSCURRENT".to_string()],
4950 created_at: now,
4951 },
4952 );
4953 current_version_id = Some(version_id);
4954 }
4955
4956 let mut tags: Vec<(String, String)> = Vec::new();
4957 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
4958 for t in arr {
4959 if let (Some(k), Some(v)) = (
4960 t.get("Key").and_then(|x| x.as_str()),
4961 t.get("Value").and_then(|x| x.as_str()),
4962 ) {
4963 tags.push((k.to_string(), v.to_string()));
4964 }
4965 }
4966 }
4967 let tags_set = !tags.is_empty();
4968
4969 let secret = Secret {
4970 name: name.clone(),
4971 arn: arn.clone(),
4972 description,
4973 kms_key_id,
4974 versions,
4975 current_version_id,
4976 tags,
4977 tags_ever_set: tags_set,
4978 deleted: false,
4979 deletion_date: None,
4980 created_at: now,
4981 last_changed_at: now,
4982 last_accessed_at: None,
4983 rotation_enabled: None,
4984 rotation_lambda_arn: None,
4985 rotation_rules: None,
4986 last_rotated_at: None,
4987 resource_policy: None,
4988 };
4989 state.secrets.insert(arn.clone(), secret);
4990
4991 Ok(ProvisionResult::new(arn.clone())
4992 .with("Id", arn.clone())
4993 .with("Name", name))
4994 }
4995
4996 fn delete_secrets_manager_secret(&self, physical_id: &str) -> Result<(), String> {
4997 let mut accounts = self.secretsmanager_state.write();
4998 let state = accounts.get_or_create(&self.account_id);
4999 state.secrets.remove(physical_id);
5000 Ok(())
5001 }
5002
5003 fn create_kinesis_stream(
5006 &self,
5007 resource: &ResourceDefinition,
5008 ) -> Result<ProvisionResult, String> {
5009 let props = &resource.properties;
5010 let stream_name = props
5011 .get("Name")
5012 .and_then(|v| v.as_str())
5013 .unwrap_or(&resource.logical_id)
5014 .to_string();
5015 let shard_count = props
5016 .get("ShardCount")
5017 .and_then(|v| v.as_i64())
5018 .unwrap_or(1) as i32;
5019 if shard_count <= 0 {
5020 return Err("ShardCount must be greater than zero".to_string());
5021 }
5022 let stream_mode = props
5023 .get("StreamModeDetails")
5024 .and_then(|v| v.get("StreamMode"))
5025 .and_then(|v| v.as_str())
5026 .unwrap_or("PROVISIONED")
5027 .to_string();
5028 let retention_period_hours = props
5029 .get("RetentionPeriodHours")
5030 .and_then(|v| v.as_i64())
5031 .unwrap_or(24) as i32;
5032
5033 let mut accounts = self.kinesis_state.write();
5034 let state = accounts.get_or_create(&self.account_id);
5035 if state.streams.contains_key(&stream_name) {
5036 return Err(format!("Stream {stream_name} already exists"));
5037 }
5038 let stream_arn = format!(
5039 "arn:aws:kinesis:{}:{}:stream/{}",
5040 state.region, state.account_id, stream_name
5041 );
5042 let stream = KinesisStream {
5043 stream_name: stream_name.clone(),
5044 stream_arn: stream_arn.clone(),
5045 stream_status: "ACTIVE".to_string(),
5046 stream_creation_timestamp: Utc::now(),
5047 retention_period_hours,
5048 stream_mode,
5049 encryption_type: "NONE".to_string(),
5050 key_id: None,
5051 shard_count,
5052 open_shard_count: shard_count,
5053 tags: BTreeMap::new(),
5054 shards: build_stream_shards(shard_count),
5055 next_shard_index: shard_count,
5056 enhanced_metrics: Vec::new(),
5057 warm_throughput_mibps: None,
5058 max_record_size_kib: None,
5059 };
5060 state.streams.insert(stream_name.clone(), stream);
5061
5062 Ok(ProvisionResult::new(stream_name).with("Arn", stream_arn))
5063 }
5064
5065 fn delete_kinesis_stream(&self, physical_id: &str) -> Result<(), String> {
5066 let mut accounts = self.kinesis_state.write();
5067 let state = accounts.get_or_create(&self.account_id);
5068 state.streams.remove(physical_id);
5069 Ok(())
5070 }
5071
5072 fn create_kinesis_stream_consumer(
5073 &self,
5074 resource: &ResourceDefinition,
5075 ) -> Result<ProvisionResult, String> {
5076 let props = &resource.properties;
5077 let stream_arn = props
5078 .get("StreamARN")
5079 .and_then(|v| v.as_str())
5080 .ok_or_else(|| "StreamARN is required".to_string())?
5081 .to_string();
5082 let consumer_name = props
5083 .get("ConsumerName")
5084 .and_then(|v| v.as_str())
5085 .ok_or_else(|| "ConsumerName is required".to_string())?
5086 .to_string();
5087
5088 let mut accounts = self.kinesis_state.write();
5089 let state = accounts.get_or_create(&self.account_id);
5090 if state
5091 .consumers
5092 .values()
5093 .any(|c| c.stream_arn == stream_arn && c.consumer_name == consumer_name)
5094 {
5095 return Err(format!(
5096 "Consumer {consumer_name} already exists on stream {stream_arn}"
5097 ));
5098 }
5099 let now = Utc::now();
5100 let consumer_arn = format!(
5101 "{}/consumer/{}:{}",
5102 stream_arn,
5103 consumer_name,
5104 now.timestamp()
5105 );
5106 let consumer = KinesisConsumer {
5107 consumer_name: consumer_name.clone(),
5108 consumer_arn: consumer_arn.clone(),
5109 consumer_status: "ACTIVE".to_string(),
5110 consumer_creation_timestamp: now,
5111 stream_arn: stream_arn.clone(),
5112 };
5113 state.consumers.insert(consumer_arn.clone(), consumer);
5114
5115 Ok(ProvisionResult::new(consumer_arn.clone())
5116 .with("ConsumerARN", consumer_arn)
5117 .with("ConsumerName", consumer_name)
5118 .with("ConsumerStatus", "ACTIVE")
5119 .with("ConsumerCreationTimestamp", now.timestamp().to_string())
5120 .with("StreamARN", stream_arn))
5121 }
5122
5123 fn delete_kinesis_stream_consumer(&self, physical_id: &str) -> Result<(), String> {
5124 let mut accounts = self.kinesis_state.write();
5125 let state = accounts.get_or_create(&self.account_id);
5126 state.consumers.remove(physical_id);
5127 Ok(())
5128 }
5129
5130 fn create_kms_key(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
5133 let input = parse_kms_key_input(&resource.properties);
5134 let (key_id, arn) =
5135 kms_provisioner::provision_key(&self.kms_state, &self.account_id, &input)?;
5136 Ok(ProvisionResult::new(key_id.clone())
5137 .with("Arn", arn)
5138 .with("KeyId", key_id))
5139 }
5140
5141 fn update_kms_key(
5147 &self,
5148 existing: &StackResource,
5149 new_def: &ResourceDefinition,
5150 ) -> Result<ProvisionResult, String> {
5151 let new_input = parse_kms_key_input(&new_def.properties);
5152 let arn = {
5153 let mut accounts = self.kms_state.write();
5154 let state = accounts.get_or_create(&self.account_id);
5155 let key = state
5156 .keys
5157 .get(&existing.physical_id)
5158 .ok_or_else(|| format!("Key '{}' does not exist", existing.physical_id))?;
5159 if key.key_spec != new_input.key_spec
5160 || key.key_usage != new_input.key_usage
5161 || key.origin != new_input.origin
5162 || key.multi_region != new_input.multi_region
5163 {
5164 return Err(
5165 "AWS::KMS::Key updates that change KeySpec, KeyUsage, Origin, or MultiRegion require replacement"
5166 .to_string(),
5167 );
5168 }
5169 key.arn.clone()
5170 };
5171 kms_provisioner::update_key_properties(
5172 &self.kms_state,
5173 &self.account_id,
5174 &existing.physical_id,
5175 kms_provisioner::KeyUpdate {
5176 description: Some(new_input.description.clone()),
5177 enabled: Some(new_input.enabled),
5178 key_rotation_enabled: Some(new_input.key_rotation_enabled),
5179 policy: new_input.policy.clone(),
5180 tags: Some(new_input.tags.clone()),
5181 },
5182 )?;
5183 Ok(ProvisionResult::new(existing.physical_id.clone())
5184 .with("Arn", arn)
5185 .with("KeyId", existing.physical_id.clone()))
5186 }
5187
5188 fn delete_kms_key(&self, physical_id: &str) -> Result<(), String> {
5189 let mut accounts = self.kms_state.write();
5190 let state = accounts.get_or_create(&self.account_id);
5191 state.keys.remove(physical_id);
5192 state.aliases.retain(|_, a| a.target_key_id != physical_id);
5193 Ok(())
5194 }
5195
5196 fn create_kms_replica_key(
5200 &self,
5201 resource: &ResourceDefinition,
5202 ) -> Result<ProvisionResult, String> {
5203 let props = &resource.properties;
5204 let primary_arn = props
5205 .get("PrimaryKeyArn")
5206 .and_then(|v| v.as_str())
5207 .ok_or_else(|| "PrimaryKeyArn is required".to_string())?
5208 .to_string();
5209 let description = props
5210 .get("Description")
5211 .and_then(|v| v.as_str())
5212 .map(str::to_string);
5213 let enabled = props
5214 .get("Enabled")
5215 .and_then(|v| v.as_bool())
5216 .unwrap_or(true);
5217 let policy = parse_key_policy(props);
5218 let tags = parse_tag_list(props);
5219
5220 let (replica_key_id, replica_arn) = kms_provisioner::provision_replica_key(
5221 &self.kms_state,
5222 &self.account_id,
5223 &primary_arn,
5224 description,
5225 enabled,
5226 policy,
5227 tags,
5228 )?;
5229 Ok(ProvisionResult::new(replica_key_id.clone())
5230 .with("KeyId", replica_key_id)
5231 .with("Arn", replica_arn))
5232 }
5233
5234 fn update_kms_replica_key(
5240 &self,
5241 existing: &StackResource,
5242 new_def: &ResourceDefinition,
5243 ) -> Result<ProvisionResult, String> {
5244 let props = &new_def.properties;
5245 let new_primary = props
5246 .get("PrimaryKeyArn")
5247 .and_then(|v| v.as_str())
5248 .ok_or_else(|| "PrimaryKeyArn is required".to_string())?;
5249 {
5252 let mut accounts = self.kms_state.write();
5253 let state = accounts.get_or_create(&self.account_id);
5254 let key = state
5255 .keys
5256 .get(&existing.physical_id)
5257 .ok_or_else(|| format!("ReplicaKey '{}' does not exist", existing.physical_id))?;
5258 if let Some(existing_region) = key.primary_region.as_deref() {
5259 let parts: Vec<&str> = new_primary.split(':').collect();
5260 if parts.len() < 4 || parts[3] != existing_region {
5261 return Err(
5262 "AWS::KMS::ReplicaKey updates that change PrimaryKeyArn require replacement"
5263 .to_string(),
5264 );
5265 }
5266 }
5267 }
5268 let description = props
5269 .get("Description")
5270 .and_then(|v| v.as_str())
5271 .map(str::to_string);
5272 let enabled = props
5273 .get("Enabled")
5274 .and_then(|v| v.as_bool())
5275 .unwrap_or(true);
5276 let policy = parse_key_policy(props);
5277 let tags = parse_tag_list(props);
5278 kms_provisioner::update_key_properties(
5279 &self.kms_state,
5280 &self.account_id,
5281 &existing.physical_id,
5282 kms_provisioner::KeyUpdate {
5283 description,
5284 enabled: Some(enabled),
5285 key_rotation_enabled: None,
5286 policy,
5287 tags: Some(tags),
5288 },
5289 )?;
5290 let arn = {
5291 let mut accounts = self.kms_state.write();
5292 let state = accounts.get_or_create(&self.account_id);
5293 state
5294 .keys
5295 .get(&existing.physical_id)
5296 .map(|k| k.arn.clone())
5297 .unwrap_or_default()
5298 };
5299 Ok(ProvisionResult::new(existing.physical_id.clone())
5300 .with("KeyId", existing.physical_id.clone())
5301 .with("Arn", arn))
5302 }
5303
5304 fn delete_kms_replica_key(&self, physical_id: &str) -> Result<(), String> {
5305 let mut accounts = self.kms_state.write();
5306 let state = accounts.get_or_create(&self.account_id);
5307 state.keys.remove(physical_id);
5308 Ok(())
5309 }
5310
5311 fn create_kms_alias(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
5312 let props = &resource.properties;
5313 let alias_name = props
5314 .get("AliasName")
5315 .and_then(|v| v.as_str())
5316 .ok_or_else(|| "AliasName is required".to_string())?
5317 .to_string();
5318 let target_input = props
5319 .get("TargetKeyId")
5320 .and_then(|v| v.as_str())
5321 .ok_or_else(|| "TargetKeyId is required".to_string())?
5322 .to_string();
5323 let alias = kms_provisioner::provision_alias(
5324 &self.kms_state,
5325 &self.account_id,
5326 &alias_name,
5327 &target_input,
5328 )?;
5329 Ok(ProvisionResult::new(alias))
5330 }
5331
5332 fn update_kms_alias(
5336 &self,
5337 existing: &StackResource,
5338 new_def: &ResourceDefinition,
5339 ) -> Result<ProvisionResult, String> {
5340 let props = &new_def.properties;
5341 let new_alias_name = props
5342 .get("AliasName")
5343 .and_then(|v| v.as_str())
5344 .ok_or_else(|| "AliasName is required".to_string())?;
5345 if new_alias_name != existing.physical_id {
5346 return Err(
5347 "AWS::KMS::Alias updates that change AliasName require replacement".to_string(),
5348 );
5349 }
5350 let target_input = props
5351 .get("TargetKeyId")
5352 .and_then(|v| v.as_str())
5353 .ok_or_else(|| "TargetKeyId is required".to_string())?;
5354 kms_provisioner::update_alias_target(
5355 &self.kms_state,
5356 &self.account_id,
5357 &existing.physical_id,
5358 target_input,
5359 )?;
5360 Ok(ProvisionResult::new(existing.physical_id.clone()))
5361 }
5362
5363 fn delete_kms_alias(&self, physical_id: &str) -> Result<(), String> {
5364 let mut accounts = self.kms_state.write();
5365 let state = accounts.get_or_create(&self.account_id);
5366 state.aliases.remove(physical_id);
5367 Ok(())
5368 }
5369
5370 fn create_ecr_repository(
5373 &self,
5374 resource: &ResourceDefinition,
5375 ) -> Result<ProvisionResult, String> {
5376 let props = &resource.properties;
5377 let repository_name = props
5378 .get("RepositoryName")
5379 .and_then(|v| v.as_str())
5380 .unwrap_or(&resource.logical_id)
5381 .to_string();
5382 let image_tag_mutability = props
5383 .get("ImageTagMutability")
5384 .and_then(|v| v.as_str())
5385 .unwrap_or("MUTABLE")
5386 .to_string();
5387 let scan_on_push = props
5388 .get("ImageScanningConfiguration")
5389 .and_then(|v| v.get("ScanOnPush"))
5390 .and_then(|v| v.as_bool())
5391 .unwrap_or(false);
5392 let encryption_type = props
5393 .get("EncryptionConfiguration")
5394 .and_then(|v| v.get("EncryptionType"))
5395 .and_then(|v| v.as_str())
5396 .unwrap_or("AES256")
5397 .to_string();
5398 let kms_key = props
5399 .get("EncryptionConfiguration")
5400 .and_then(|v| v.get("KmsKey"))
5401 .and_then(|v| v.as_str())
5402 .map(|s| s.to_string());
5403 let policy_text = props
5404 .get("RepositoryPolicyText")
5405 .map(|v| {
5406 if v.is_string() {
5407 v.as_str().unwrap_or("").to_string()
5408 } else {
5409 serde_json::to_string(v).unwrap_or_default()
5410 }
5411 })
5412 .filter(|s| !s.is_empty());
5413 let lifecycle_policy = props
5414 .get("LifecyclePolicy")
5415 .and_then(|v| v.get("LifecyclePolicyText"))
5416 .and_then(|v| v.as_str())
5417 .map(|s| s.to_string());
5418 let mut tags: BTreeMap<String, String> = BTreeMap::new();
5419 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
5420 for t in arr {
5421 if let (Some(k), Some(v)) = (
5422 t.get("Key").and_then(|x| x.as_str()),
5423 t.get("Value").and_then(|x| x.as_str()),
5424 ) {
5425 tags.insert(k.to_string(), v.to_string());
5426 }
5427 }
5428 }
5429
5430 let empty_on_delete = props
5431 .get("EmptyOnDelete")
5432 .and_then(|v| v.as_bool())
5433 .unwrap_or(false);
5434
5435 let mut accounts = self.ecr_state.write();
5436 let state = accounts.get_or_create(&self.account_id);
5437 if state.repositories.contains_key(&repository_name) {
5438 return Err(format!("Repository {repository_name} already exists"));
5439 }
5440 let arn = state.repository_arn(&repository_name);
5441 let registry_id = state.account_id.clone();
5442 let endpoint = format!(
5443 "{}.dkr.ecr.{}.amazonaws.com",
5444 state.account_id, state.region
5445 );
5446 let mut repo = Repository::new(&repository_name, arn.clone(), ®istry_id, &endpoint);
5447 repo.image_tag_mutability = image_tag_mutability;
5448 repo.image_scanning_configuration.scan_on_push = scan_on_push;
5449 repo.encryption_configuration.encryption_type = encryption_type;
5450 repo.encryption_configuration.kms_key = kms_key;
5451 repo.policy = policy_text;
5452 if let Some(policy) = lifecycle_policy.as_ref() {
5455 let prune = fakecloud_ecr::evaluate_lifecycle_policy(&repo, policy);
5456 for digest in &prune {
5457 repo.images.remove(digest);
5458 repo.image_tags.retain(|_, d| d != digest);
5459 }
5460 repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
5461 }
5462 repo.lifecycle_policy = lifecycle_policy;
5463 repo.tags = tags;
5464 let uri = repo.repository_uri.clone();
5465 state.repositories.insert(repository_name.clone(), repo);
5466
5467 Ok(ProvisionResult::new(repository_name)
5468 .with("Arn", arn)
5469 .with("RepositoryUri", uri)
5470 .with("RegistryId", registry_id)
5471 .with("EmptyOnDelete", empty_on_delete.to_string()))
5472 }
5473
5474 fn delete_ecr_repository(&self, physical_id: &str) -> Result<(), String> {
5475 let mut accounts = self.ecr_state.write();
5476 let state = accounts.get_or_create(&self.account_id);
5477 state.repositories.remove(physical_id);
5478 Ok(())
5479 }
5480
5481 fn create_ecr_repository_policy(
5485 &self,
5486 resource: &ResourceDefinition,
5487 ) -> Result<ProvisionResult, String> {
5488 let props = &resource.properties;
5489 let repository_name = props
5490 .get("RepositoryName")
5491 .and_then(|v| v.as_str())
5492 .ok_or_else(|| "RepositoryName is required".to_string())?
5493 .to_string();
5494 let policy_text = props
5495 .get("PolicyText")
5496 .map(|v| {
5497 if v.is_string() {
5498 v.as_str().unwrap_or("").to_string()
5499 } else {
5500 serde_json::to_string(v).unwrap_or_default()
5501 }
5502 })
5503 .ok_or_else(|| "PolicyText is required".to_string())?;
5504 let mut accounts = self.ecr_state.write();
5505 let state = accounts.get_or_create(&self.account_id);
5506 let repo = state
5507 .repositories
5508 .get_mut(&repository_name)
5509 .ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
5510 repo.policy = Some(policy_text);
5511 Ok(ProvisionResult::new(format!(
5512 "{}/{}",
5513 self.account_id, repository_name
5514 )))
5515 }
5516
5517 fn delete_ecr_repository_policy(&self, physical_id: &str) -> Result<(), String> {
5518 let repository_name = physical_id
5519 .split_once('/')
5520 .map(|(_, n)| n.to_string())
5521 .unwrap_or_else(|| physical_id.to_string());
5522 let mut accounts = self.ecr_state.write();
5523 let state = accounts.get_or_create(&self.account_id);
5524 if let Some(repo) = state.repositories.get_mut(&repository_name) {
5525 repo.policy = None;
5526 }
5527 Ok(())
5528 }
5529
5530 fn create_ecr_registry_policy(
5533 &self,
5534 resource: &ResourceDefinition,
5535 ) -> Result<ProvisionResult, String> {
5536 let props = &resource.properties;
5537 let policy_text = props
5538 .get("PolicyText")
5539 .map(|v| {
5540 if v.is_string() {
5541 v.as_str().unwrap_or("").to_string()
5542 } else {
5543 serde_json::to_string(v).unwrap_or_default()
5544 }
5545 })
5546 .ok_or_else(|| "PolicyText is required".to_string())?;
5547 let mut accounts = self.ecr_state.write();
5548 let state = accounts.get_or_create(&self.account_id);
5549 state.registry_policy = Some(policy_text);
5550 Ok(ProvisionResult::new(self.account_id.clone())
5551 .with("RegistryId", self.account_id.clone()))
5552 }
5553
5554 fn delete_ecr_registry_policy(&self) -> Result<(), String> {
5555 let mut accounts = self.ecr_state.write();
5556 let state = accounts.get_or_create(&self.account_id);
5557 state.registry_policy = None;
5558 Ok(())
5559 }
5560
5561 fn create_ecr_replication_configuration(
5563 &self,
5564 resource: &ResourceDefinition,
5565 ) -> Result<ProvisionResult, String> {
5566 use fakecloud_ecr::state::{
5567 ReplicationConfiguration, ReplicationDestination, ReplicationRule, RepositoryFilter,
5568 };
5569 let cfg = resource
5570 .properties
5571 .get("ReplicationConfiguration")
5572 .ok_or_else(|| "ReplicationConfiguration is required".to_string())?;
5573 let rules: Vec<ReplicationRule> = cfg
5574 .get("Rules")
5575 .and_then(|v| v.as_array())
5576 .map(|arr| {
5577 arr.iter()
5578 .map(|r| {
5579 let destinations: Vec<ReplicationDestination> = r
5580 .get("Destinations")
5581 .and_then(|v| v.as_array())
5582 .map(|d| {
5583 d.iter()
5584 .map(|dest| ReplicationDestination {
5585 region: dest
5586 .get("Region")
5587 .and_then(|v| v.as_str())
5588 .unwrap_or_default()
5589 .to_string(),
5590 registry_id: dest
5591 .get("RegistryId")
5592 .and_then(|v| v.as_str())
5593 .unwrap_or_default()
5594 .to_string(),
5595 })
5596 .collect()
5597 })
5598 .unwrap_or_default();
5599 let repository_filters: Vec<RepositoryFilter> = r
5600 .get("RepositoryFilters")
5601 .and_then(|v| v.as_array())
5602 .map(|f| {
5603 f.iter()
5604 .map(|f| RepositoryFilter {
5605 filter: f
5606 .get("Filter")
5607 .and_then(|v| v.as_str())
5608 .unwrap_or_default()
5609 .to_string(),
5610 filter_type: f
5611 .get("FilterType")
5612 .and_then(|v| v.as_str())
5613 .unwrap_or_default()
5614 .to_string(),
5615 })
5616 .collect()
5617 })
5618 .unwrap_or_default();
5619 ReplicationRule {
5620 destinations,
5621 repository_filters,
5622 }
5623 })
5624 .collect()
5625 })
5626 .unwrap_or_default();
5627 let mut accounts = self.ecr_state.write();
5628 let state = accounts.get_or_create(&self.account_id);
5629 state.replication_configuration = Some(ReplicationConfiguration { rules });
5630 Ok(ProvisionResult::new(self.account_id.clone()))
5631 }
5632
5633 fn delete_ecr_replication_configuration(&self) -> Result<(), String> {
5634 let mut accounts = self.ecr_state.write();
5635 let state = accounts.get_or_create(&self.account_id);
5636 state.replication_configuration = None;
5637 Ok(())
5638 }
5639
5640 fn create_ecr_pull_through_cache_rule(
5641 &self,
5642 resource: &ResourceDefinition,
5643 ) -> Result<ProvisionResult, String> {
5644 use fakecloud_ecr::state::PullThroughCacheRule;
5645 let props = &resource.properties;
5646 let prefix = props
5647 .get("EcrRepositoryPrefix")
5648 .and_then(|v| v.as_str())
5649 .ok_or_else(|| "EcrRepositoryPrefix is required".to_string())?
5650 .to_string();
5651 let upstream_url = props
5652 .get("UpstreamRegistryUrl")
5653 .and_then(|v| v.as_str())
5654 .ok_or_else(|| "UpstreamRegistryUrl is required".to_string())?
5655 .to_string();
5656 let upstream_registry = props
5657 .get("UpstreamRegistry")
5658 .and_then(|v| v.as_str())
5659 .map(|s| s.to_string());
5660 let credential_arn = props
5661 .get("CredentialArn")
5662 .and_then(|v| v.as_str())
5663 .map(|s| s.to_string());
5664 let custom_role_arn = props
5665 .get("CustomRoleArn")
5666 .and_then(|v| v.as_str())
5667 .map(|s| s.to_string());
5668 let now = Utc::now();
5669 let rule = PullThroughCacheRule {
5670 ecr_repository_prefix: prefix.clone(),
5671 upstream_registry_url: upstream_url,
5672 upstream_registry,
5673 credential_arn,
5674 created_at: now,
5675 updated_at: now,
5676 custom_role_arn,
5677 };
5678 let mut accounts = self.ecr_state.write();
5679 let state = accounts.get_or_create(&self.account_id);
5680 state.pull_through_cache_rules.insert(prefix.clone(), rule);
5681 Ok(ProvisionResult::new(prefix))
5682 }
5683
5684 fn delete_ecr_pull_through_cache_rule(&self, physical_id: &str) -> Result<(), String> {
5685 let mut accounts = self.ecr_state.write();
5686 let state = accounts.get_or_create(&self.account_id);
5687 state.pull_through_cache_rules.remove(physical_id);
5688 Ok(())
5689 }
5690
5691 fn create_ecr_lifecycle_policy(
5697 &self,
5698 resource: &ResourceDefinition,
5699 ) -> Result<ProvisionResult, String> {
5700 let props = &resource.properties;
5701 let repository_name = props
5702 .get("RepositoryName")
5703 .and_then(|v| v.as_str())
5704 .ok_or_else(|| "RepositoryName is required".to_string())?
5705 .to_string();
5706 let policy_text = props
5707 .get("LifecyclePolicyText")
5708 .map(|v| {
5709 if v.is_string() {
5710 v.as_str().unwrap_or("").to_string()
5711 } else {
5712 serde_json::to_string(v).unwrap_or_default()
5713 }
5714 })
5715 .ok_or_else(|| "LifecyclePolicyText is required".to_string())?;
5716 let _registry_id = props
5719 .get("RegistryId")
5720 .and_then(|v| v.as_str())
5721 .map(|s| s.to_string());
5722 let mut accounts = self.ecr_state.write();
5723 let state = accounts.get_or_create(&self.account_id);
5724 let repo = state
5725 .repositories
5726 .get_mut(&repository_name)
5727 .ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
5728 let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, &policy_text);
5731 for digest in &prune {
5732 repo.images.remove(digest);
5733 repo.image_tags.retain(|_, d| d != digest);
5734 }
5735 repo.lifecycle_policy = Some(policy_text);
5736 repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
5737 let registry_id = repo.registry_id.clone();
5738 Ok(
5739 ProvisionResult::new(format!("{}/{}", self.account_id, repository_name))
5740 .with("RepositoryName", repository_name)
5741 .with("RegistryId", registry_id),
5742 )
5743 }
5744
5745 fn delete_ecr_lifecycle_policy(&self, physical_id: &str) -> Result<(), String> {
5746 let repository_name = physical_id
5747 .split_once('/')
5748 .map(|(_, n)| n.to_string())
5749 .unwrap_or_else(|| physical_id.to_string());
5750 let mut accounts = self.ecr_state.write();
5751 let state = accounts.get_or_create(&self.account_id);
5752 if let Some(repo) = state.repositories.get_mut(&repository_name) {
5753 repo.lifecycle_policy = None;
5754 repo.lifecycle_policy_last_evaluated_at = None;
5755 }
5756 Ok(())
5757 }
5758
5759 fn create_ecr_registry_scanning_configuration(
5763 &self,
5764 resource: &ResourceDefinition,
5765 ) -> Result<ProvisionResult, String> {
5766 use fakecloud_ecr::state::{
5767 RegistryScanningConfiguration, RegistryScanningRule, RepositoryFilter,
5768 };
5769 let props = &resource.properties;
5770 let scan_type = props
5771 .get("ScanType")
5772 .and_then(|v| v.as_str())
5773 .unwrap_or("BASIC")
5774 .to_string();
5775 let rules: Vec<RegistryScanningRule> = props
5776 .get("Rules")
5777 .and_then(|v| v.as_array())
5778 .map(|arr| {
5779 arr.iter()
5780 .map(|r| {
5781 let scan_frequency = r
5782 .get("ScanFrequency")
5783 .and_then(|v| v.as_str())
5784 .unwrap_or("MANUAL")
5785 .to_string();
5786 let repository_filters: Vec<RepositoryFilter> = r
5787 .get("RepositoryFilters")
5788 .and_then(|v| v.as_array())
5789 .map(|f| {
5790 f.iter()
5791 .map(|f| RepositoryFilter {
5792 filter: f
5793 .get("Filter")
5794 .and_then(|v| v.as_str())
5795 .unwrap_or_default()
5796 .to_string(),
5797 filter_type: f
5798 .get("FilterType")
5799 .and_then(|v| v.as_str())
5800 .unwrap_or_default()
5801 .to_string(),
5802 })
5803 .collect()
5804 })
5805 .unwrap_or_default();
5806 RegistryScanningRule {
5807 scan_frequency,
5808 repository_filters,
5809 }
5810 })
5811 .collect()
5812 })
5813 .unwrap_or_default();
5814 let mut accounts = self.ecr_state.write();
5815 let state = accounts.get_or_create(&self.account_id);
5816 state.registry_scanning_configuration = RegistryScanningConfiguration { scan_type, rules };
5817 Ok(ProvisionResult::new(self.account_id.clone()))
5818 }
5819
5820 fn delete_ecr_registry_scanning_configuration(&self) -> Result<(), String> {
5821 use fakecloud_ecr::state::RegistryScanningConfiguration;
5822 let mut accounts = self.ecr_state.write();
5823 let state = accounts.get_or_create(&self.account_id);
5824 state.registry_scanning_configuration = RegistryScanningConfiguration::default();
5826 Ok(())
5827 }
5828
5829 fn update_ecr_repository(
5835 &self,
5836 existing: &StackResource,
5837 resource: &ResourceDefinition,
5838 ) -> Result<ProvisionResult, String> {
5839 let props = &resource.properties;
5840 let repository_name = existing.physical_id.clone();
5841 let mut accounts = self.ecr_state.write();
5842 let state = accounts.get_or_create(&self.account_id);
5843 let repo = state
5844 .repositories
5845 .get_mut(&repository_name)
5846 .ok_or_else(|| format!("Repository {repository_name} no longer exists"))?;
5847 if let Some(s) = props.get("ImageTagMutability").and_then(|v| v.as_str()) {
5848 repo.image_tag_mutability = s.to_string();
5849 }
5850 if let Some(b) = props
5851 .get("ImageScanningConfiguration")
5852 .and_then(|v| v.get("ScanOnPush"))
5853 .and_then(|v| v.as_bool())
5854 {
5855 repo.image_scanning_configuration.scan_on_push = b;
5856 }
5857 if let Some(cfg) = props.get("EncryptionConfiguration") {
5858 if let Some(s) = cfg.get("EncryptionType").and_then(|v| v.as_str()) {
5859 repo.encryption_configuration.encryption_type = s.to_string();
5860 }
5861 if let Some(s) = cfg.get("KmsKey").and_then(|v| v.as_str()) {
5862 repo.encryption_configuration.kms_key = Some(s.to_string());
5863 }
5864 }
5865 if let Some(v) = props.get("RepositoryPolicyText") {
5866 let text = if v.is_string() {
5867 v.as_str().unwrap_or("").to_string()
5868 } else {
5869 serde_json::to_string(v).unwrap_or_default()
5870 };
5871 repo.policy = if text.is_empty() { None } else { Some(text) };
5872 }
5873 if let Some(text) = props
5874 .get("LifecyclePolicy")
5875 .and_then(|v| v.get("LifecyclePolicyText"))
5876 .and_then(|v| v.as_str())
5877 {
5878 let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, text);
5879 for digest in &prune {
5880 repo.images.remove(digest);
5881 repo.image_tags.retain(|_, d| d != digest);
5882 }
5883 repo.lifecycle_policy = Some(text.to_string());
5884 repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
5885 }
5886 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
5887 let mut tags: BTreeMap<String, String> = BTreeMap::new();
5888 for t in arr {
5889 if let (Some(k), Some(v)) = (
5890 t.get("Key").and_then(|x| x.as_str()),
5891 t.get("Value").and_then(|x| x.as_str()),
5892 ) {
5893 tags.insert(k.to_string(), v.to_string());
5894 }
5895 }
5896 repo.tags = tags;
5897 }
5898 let arn = repo.repository_arn.clone();
5899 let uri = repo.repository_uri.clone();
5900 let registry_id = repo.registry_id.clone();
5901 Ok(ProvisionResult::new(repository_name)
5902 .with("Arn", arn)
5903 .with("RepositoryUri", uri)
5904 .with("RegistryId", registry_id))
5905 }
5906
5907 fn update_ecr_repository_policy(
5908 &self,
5909 existing: &StackResource,
5910 resource: &ResourceDefinition,
5911 ) -> Result<ProvisionResult, String> {
5912 let props = &resource.properties;
5913 let physical_id = existing.physical_id.clone();
5914 let repository_name = physical_id
5915 .split_once('/')
5916 .map(|(_, n)| n.to_string())
5917 .unwrap_or_else(|| physical_id.clone());
5918 let policy_text = props
5919 .get("PolicyText")
5920 .map(|v| {
5921 if v.is_string() {
5922 v.as_str().unwrap_or("").to_string()
5923 } else {
5924 serde_json::to_string(v).unwrap_or_default()
5925 }
5926 })
5927 .ok_or_else(|| "PolicyText is required".to_string())?;
5928 let mut accounts = self.ecr_state.write();
5929 let state = accounts.get_or_create(&self.account_id);
5930 let repo = state
5931 .repositories
5932 .get_mut(&repository_name)
5933 .ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
5934 repo.policy = Some(policy_text);
5935 Ok(ProvisionResult::new(physical_id))
5936 }
5937
5938 fn update_ecr_lifecycle_policy(
5939 &self,
5940 existing: &StackResource,
5941 resource: &ResourceDefinition,
5942 ) -> Result<ProvisionResult, String> {
5943 let props = &resource.properties;
5944 let physical_id = existing.physical_id.clone();
5945 let repository_name = physical_id
5946 .split_once('/')
5947 .map(|(_, n)| n.to_string())
5948 .unwrap_or_else(|| physical_id.clone());
5949 let policy_text = props
5950 .get("LifecyclePolicyText")
5951 .map(|v| {
5952 if v.is_string() {
5953 v.as_str().unwrap_or("").to_string()
5954 } else {
5955 serde_json::to_string(v).unwrap_or_default()
5956 }
5957 })
5958 .ok_or_else(|| "LifecyclePolicyText is required".to_string())?;
5959 let mut accounts = self.ecr_state.write();
5960 let state = accounts.get_or_create(&self.account_id);
5961 let repo = state
5962 .repositories
5963 .get_mut(&repository_name)
5964 .ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
5965 let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, &policy_text);
5966 for digest in &prune {
5967 repo.images.remove(digest);
5968 repo.image_tags.retain(|_, d| d != digest);
5969 }
5970 repo.lifecycle_policy = Some(policy_text);
5971 repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
5972 let registry_id = repo.registry_id.clone();
5973 Ok(ProvisionResult::new(physical_id)
5974 .with("RepositoryName", repository_name)
5975 .with("RegistryId", registry_id))
5976 }
5977
5978 fn update_ecr_registry_policy(
5979 &self,
5980 existing: &StackResource,
5981 resource: &ResourceDefinition,
5982 ) -> Result<ProvisionResult, String> {
5983 let props = &resource.properties;
5984 let policy_text = props
5985 .get("PolicyText")
5986 .map(|v| {
5987 if v.is_string() {
5988 v.as_str().unwrap_or("").to_string()
5989 } else {
5990 serde_json::to_string(v).unwrap_or_default()
5991 }
5992 })
5993 .ok_or_else(|| "PolicyText is required".to_string())?;
5994 let mut accounts = self.ecr_state.write();
5995 let state = accounts.get_or_create(&self.account_id);
5996 state.registry_policy = Some(policy_text);
5997 Ok(ProvisionResult::new(existing.physical_id.clone())
5998 .with("RegistryId", self.account_id.clone()))
5999 }
6000
6001 fn update_ecr_replication_configuration(
6002 &self,
6003 existing: &StackResource,
6004 resource: &ResourceDefinition,
6005 ) -> Result<ProvisionResult, String> {
6006 let result = self.create_ecr_replication_configuration(resource)?;
6008 Ok(ProvisionResult::new(existing.physical_id.clone()).merge_attributes(result.attributes))
6009 }
6010
6011 fn update_ecr_registry_scanning_configuration(
6012 &self,
6013 existing: &StackResource,
6014 resource: &ResourceDefinition,
6015 ) -> Result<ProvisionResult, String> {
6016 let result = self.create_ecr_registry_scanning_configuration(resource)?;
6017 Ok(ProvisionResult::new(existing.physical_id.clone()).merge_attributes(result.attributes))
6018 }
6019
6020 fn update_ecr_pull_through_cache_rule(
6021 &self,
6022 existing: &StackResource,
6023 resource: &ResourceDefinition,
6024 ) -> Result<ProvisionResult, String> {
6025 let props = &resource.properties;
6026 let prefix = existing.physical_id.clone();
6027 let mut accounts = self.ecr_state.write();
6028 let state = accounts.get_or_create(&self.account_id);
6029 let rule = state
6030 .pull_through_cache_rules
6031 .get_mut(&prefix)
6032 .ok_or_else(|| format!("PullThroughCacheRule {prefix} no longer exists"))?;
6033 if let Some(s) = props.get("UpstreamRegistryUrl").and_then(|v| v.as_str()) {
6034 rule.upstream_registry_url = s.to_string();
6035 }
6036 if let Some(s) = props.get("UpstreamRegistry").and_then(|v| v.as_str()) {
6037 rule.upstream_registry = Some(s.to_string());
6038 }
6039 if let Some(s) = props.get("CredentialArn").and_then(|v| v.as_str()) {
6040 rule.credential_arn = Some(s.to_string());
6041 }
6042 if let Some(s) = props.get("CustomRoleArn").and_then(|v| v.as_str()) {
6043 rule.custom_role_arn = Some(s.to_string());
6044 }
6045 rule.updated_at = Utc::now();
6046 Ok(ProvisionResult::new(prefix))
6047 }
6048
6049 fn get_att_ecr_repository(&self, physical_id: &str, attribute: &str) -> Option<String> {
6050 let mut accounts = self.ecr_state.write();
6051 let state = accounts.get_or_create(&self.account_id);
6052 let repo = state.repositories.get(physical_id)?;
6053 match attribute {
6054 "Arn" => Some(repo.repository_arn.clone()),
6055 "RepositoryUri" => Some(repo.repository_uri.clone()),
6056 "RegistryId" => Some(repo.registry_id.clone()),
6057 _ => None,
6058 }
6059 }
6060
6061 fn create_cloudwatch_alarm(
6064 &self,
6065 resource: &ResourceDefinition,
6066 ) -> Result<ProvisionResult, String> {
6067 let props = &resource.properties;
6068 let alarm_name = props
6069 .get("AlarmName")
6070 .and_then(|v| v.as_str())
6071 .unwrap_or(&resource.logical_id)
6072 .to_string();
6073 let alarm_description = props
6074 .get("AlarmDescription")
6075 .and_then(|v| v.as_str())
6076 .map(|s| s.to_string());
6077 let actions_enabled = props
6078 .get("ActionsEnabled")
6079 .and_then(|v| v.as_bool())
6080 .unwrap_or(true);
6081 let str_array = |key: &str| -> Vec<String> {
6082 props
6083 .get(key)
6084 .and_then(|v| v.as_array())
6085 .map(|arr| {
6086 arr.iter()
6087 .filter_map(|x| x.as_str().map(|s| s.to_string()))
6088 .collect()
6089 })
6090 .unwrap_or_default()
6091 };
6092 let alarm_actions = str_array("AlarmActions");
6093 let ok_actions = str_array("OKActions");
6094 let insufficient_data_actions = str_array("InsufficientDataActions");
6095
6096 let metric_name = props
6097 .get("MetricName")
6098 .and_then(|v| v.as_str())
6099 .map(|s| s.to_string());
6100 let namespace = props
6101 .get("Namespace")
6102 .and_then(|v| v.as_str())
6103 .map(|s| s.to_string());
6104 let statistic = props
6105 .get("Statistic")
6106 .and_then(|v| v.as_str())
6107 .map(|s| s.to_string());
6108 let extended_statistic = props
6109 .get("ExtendedStatistic")
6110 .and_then(|v| v.as_str())
6111 .map(|s| s.to_string());
6112 let unit = props
6113 .get("Unit")
6114 .and_then(|v| v.as_str())
6115 .map(|s| s.to_string());
6116 let period = props.get("Period").and_then(|v| v.as_i64());
6117 let evaluation_periods = props
6118 .get("EvaluationPeriods")
6119 .and_then(|v| v.as_i64())
6120 .unwrap_or(1);
6121 let datapoints_to_alarm = props.get("DatapointsToAlarm").and_then(|v| v.as_i64());
6122 let threshold = props.get("Threshold").and_then(|v| v.as_f64());
6123 let comparison_operator = props
6124 .get("ComparisonOperator")
6125 .and_then(|v| v.as_str())
6126 .unwrap_or("GreaterThanThreshold")
6127 .to_string();
6128 let treat_missing_data = props
6129 .get("TreatMissingData")
6130 .and_then(|v| v.as_str())
6131 .map(|s| s.to_string());
6132 let evaluate_low_sample_count_percentile = props
6133 .get("EvaluateLowSampleCountPercentile")
6134 .and_then(|v| v.as_str())
6135 .map(|s| s.to_string());
6136
6137 let mut dimensions: BTreeMap<String, String> = BTreeMap::new();
6138 if let Some(arr) = props.get("Dimensions").and_then(|v| v.as_array()) {
6139 for d in arr {
6140 if let (Some(k), Some(v)) = (
6141 d.get("Name").and_then(|x| x.as_str()),
6142 d.get("Value").and_then(|x| x.as_str()),
6143 ) {
6144 dimensions.insert(k.to_string(), v.to_string());
6145 }
6146 }
6147 }
6148
6149 let mut accounts = self.cloudwatch_state.write();
6150 let state = accounts.get_or_create(&self.account_id);
6151 let alarm_arn = format!(
6152 "arn:aws:cloudwatch:{}:{}:alarm:{}",
6153 self.region, self.account_id, alarm_name
6154 );
6155 let now = Utc::now();
6156 let alarm = MetricAlarm {
6157 alarm_name: alarm_name.clone(),
6158 alarm_arn: alarm_arn.clone(),
6159 alarm_description,
6160 actions_enabled,
6161 ok_actions,
6162 alarm_actions,
6163 insufficient_data_actions,
6164 state_value: AlarmState::InsufficientData,
6165 state_reason: "Unchecked: Initial alarm creation".to_string(),
6166 state_updated_timestamp: now,
6167 metric_name,
6168 namespace,
6169 statistic,
6170 extended_statistic,
6171 dimensions,
6172 period,
6173 unit,
6174 evaluation_periods,
6175 datapoints_to_alarm,
6176 threshold,
6177 comparison_operator,
6178 treat_missing_data,
6179 evaluate_low_sample_count_percentile,
6180 configuration_updated_timestamp: now,
6181 alarm_configuration_updated_timestamp: now,
6182 };
6183 let region_alarms = state.alarms_in_mut(&self.region);
6184 if region_alarms.contains_key(&alarm_name) {
6185 return Err(format!("Alarm {alarm_name} already exists"));
6186 }
6187 region_alarms.insert(alarm_name.clone(), alarm);
6188
6189 Ok(ProvisionResult::new(alarm_name).with("Arn", alarm_arn))
6190 }
6191
6192 fn delete_cloudwatch_alarm(&self, physical_id: &str) -> Result<(), String> {
6193 let mut accounts = self.cloudwatch_state.write();
6194 let state = accounts.get_or_create(&self.account_id);
6195 state.alarms_in_mut(&self.region).remove(physical_id);
6196 Ok(())
6197 }
6198
6199 fn create_cloudwatch_dashboard(
6200 &self,
6201 resource: &ResourceDefinition,
6202 ) -> Result<ProvisionResult, String> {
6203 let props = &resource.properties;
6204 let dashboard_name = props
6205 .get("DashboardName")
6206 .and_then(|v| v.as_str())
6207 .map(String::from)
6208 .unwrap_or_else(|| {
6209 let suffix = Uuid::new_v4().simple().to_string();
6210 format!("{}-{}", resource.logical_id, &suffix[..8])
6211 });
6212 let body = props
6214 .get("DashboardBody")
6215 .ok_or("DashboardBody is required")?;
6216 let body_str = if let Some(s) = body.as_str() {
6217 s.to_string()
6218 } else {
6219 serde_json::to_string(body).map_err(|e| format!("invalid DashboardBody: {e}"))?
6220 };
6221 serde_json::from_str::<serde_json::Value>(&body_str)
6223 .map_err(|e| format!("DashboardBody must be valid JSON: {e}"))?;
6224
6225 let arn = format!(
6226 "arn:aws:cloudwatch::{}:dashboard/{dashboard_name}",
6227 self.account_id
6228 );
6229 let dashboard = Dashboard {
6230 name: dashboard_name.clone(),
6231 arn: arn.clone(),
6232 size_bytes: body_str.len() as i64,
6233 body: body_str,
6234 last_modified: Utc::now(),
6235 };
6236
6237 let mut accounts = self.cloudwatch_state.write();
6238 let state = accounts.get_or_create(&self.account_id);
6239 state.dashboards.insert(dashboard_name.clone(), dashboard);
6240
6241 Ok(ProvisionResult::new(dashboard_name).with("Arn", arn))
6242 }
6243
6244 fn delete_cloudwatch_dashboard(&self, physical_id: &str) -> Result<(), String> {
6245 let mut accounts = self.cloudwatch_state.write();
6246 let state = accounts.get_or_create(&self.account_id);
6247 state.dashboards.remove(physical_id);
6248 Ok(())
6249 }
6250
6251 fn update_cloudwatch_alarm(
6252 &self,
6253 existing: &StackResource,
6254 new_def: &ResourceDefinition,
6255 ) -> Result<ProvisionResult, String> {
6256 let props = &new_def.properties;
6257 let new_alarm_name = props
6259 .get("AlarmName")
6260 .and_then(|v| v.as_str())
6261 .unwrap_or(&new_def.logical_id);
6262 if new_alarm_name != existing.physical_id {
6263 return Err(
6264 "AWS::CloudWatch::Alarm updates that change AlarmName require replacement"
6265 .to_string(),
6266 );
6267 }
6268
6269 let str_array = |key: &str| -> Vec<String> {
6270 props
6271 .get(key)
6272 .and_then(|v| v.as_array())
6273 .map(|arr| {
6274 arr.iter()
6275 .filter_map(|x| x.as_str().map(|s| s.to_string()))
6276 .collect()
6277 })
6278 .unwrap_or_default()
6279 };
6280 let alarm_description = props
6281 .get("AlarmDescription")
6282 .and_then(|v| v.as_str())
6283 .map(|s| s.to_string());
6284 let actions_enabled = props
6285 .get("ActionsEnabled")
6286 .and_then(|v| v.as_bool())
6287 .unwrap_or(true);
6288 let alarm_actions = str_array("AlarmActions");
6289 let ok_actions = str_array("OKActions");
6290 let insufficient_data_actions = str_array("InsufficientDataActions");
6291 let metric_name = props
6292 .get("MetricName")
6293 .and_then(|v| v.as_str())
6294 .map(|s| s.to_string());
6295 let namespace = props
6296 .get("Namespace")
6297 .and_then(|v| v.as_str())
6298 .map(|s| s.to_string());
6299 let statistic = props
6300 .get("Statistic")
6301 .and_then(|v| v.as_str())
6302 .map(|s| s.to_string());
6303 let extended_statistic = props
6304 .get("ExtendedStatistic")
6305 .and_then(|v| v.as_str())
6306 .map(|s| s.to_string());
6307 let unit = props
6308 .get("Unit")
6309 .and_then(|v| v.as_str())
6310 .map(|s| s.to_string());
6311 let period = props.get("Period").and_then(|v| v.as_i64());
6312 let evaluation_periods = props
6313 .get("EvaluationPeriods")
6314 .and_then(|v| v.as_i64())
6315 .unwrap_or(1);
6316 let datapoints_to_alarm = props.get("DatapointsToAlarm").and_then(|v| v.as_i64());
6317 let threshold = props.get("Threshold").and_then(|v| v.as_f64());
6318 let comparison_operator = props
6319 .get("ComparisonOperator")
6320 .and_then(|v| v.as_str())
6321 .unwrap_or("GreaterThanThreshold")
6322 .to_string();
6323 let treat_missing_data = props
6324 .get("TreatMissingData")
6325 .and_then(|v| v.as_str())
6326 .map(|s| s.to_string());
6327 let evaluate_low_sample_count_percentile = props
6328 .get("EvaluateLowSampleCountPercentile")
6329 .and_then(|v| v.as_str())
6330 .map(|s| s.to_string());
6331
6332 let mut dimensions: BTreeMap<String, String> = BTreeMap::new();
6333 if let Some(arr) = props.get("Dimensions").and_then(|v| v.as_array()) {
6334 for d in arr {
6335 if let (Some(k), Some(v)) = (
6336 d.get("Name").and_then(|x| x.as_str()),
6337 d.get("Value").and_then(|x| x.as_str()),
6338 ) {
6339 dimensions.insert(k.to_string(), v.to_string());
6340 }
6341 }
6342 }
6343
6344 let mut accounts = self.cloudwatch_state.write();
6345 let state = accounts.get_or_create(&self.account_id);
6346 let region_alarms = state.alarms_in_mut(&self.region);
6347 let alarm = region_alarms
6348 .get_mut(&existing.physical_id)
6349 .ok_or_else(|| format!("Alarm {} not found", existing.physical_id))?;
6350 let now = Utc::now();
6351 alarm.alarm_description = alarm_description;
6352 alarm.actions_enabled = actions_enabled;
6353 alarm.ok_actions = ok_actions;
6354 alarm.alarm_actions = alarm_actions;
6355 alarm.insufficient_data_actions = insufficient_data_actions;
6356 alarm.metric_name = metric_name;
6357 alarm.namespace = namespace;
6358 alarm.statistic = statistic;
6359 alarm.extended_statistic = extended_statistic;
6360 alarm.dimensions = dimensions;
6361 alarm.period = period;
6362 alarm.unit = unit;
6363 alarm.evaluation_periods = evaluation_periods;
6364 alarm.datapoints_to_alarm = datapoints_to_alarm;
6365 alarm.threshold = threshold;
6366 alarm.comparison_operator = comparison_operator;
6367 alarm.treat_missing_data = treat_missing_data;
6368 alarm.evaluate_low_sample_count_percentile = evaluate_low_sample_count_percentile;
6369 alarm.configuration_updated_timestamp = now;
6370 alarm.alarm_configuration_updated_timestamp = now;
6371
6372 let alarm_arn = alarm.alarm_arn.clone();
6373 Ok(ProvisionResult::new(existing.physical_id.clone()).with("Arn", alarm_arn))
6374 }
6375
6376 fn update_cloudwatch_dashboard(
6377 &self,
6378 existing: &StackResource,
6379 new_def: &ResourceDefinition,
6380 ) -> Result<ProvisionResult, String> {
6381 let props = &new_def.properties;
6382 if let Some(new_name) = props.get("DashboardName").and_then(|v| v.as_str()) {
6384 if new_name != existing.physical_id {
6385 return Err(
6386 "AWS::CloudWatch::Dashboard updates that change DashboardName require replacement"
6387 .to_string(),
6388 );
6389 }
6390 }
6391 let body = props
6392 .get("DashboardBody")
6393 .ok_or("DashboardBody is required")?;
6394 let body_str = if let Some(s) = body.as_str() {
6395 s.to_string()
6396 } else {
6397 serde_json::to_string(body).map_err(|e| format!("invalid DashboardBody: {e}"))?
6398 };
6399 serde_json::from_str::<serde_json::Value>(&body_str)
6400 .map_err(|e| format!("DashboardBody must be valid JSON: {e}"))?;
6401
6402 let mut accounts = self.cloudwatch_state.write();
6403 let state = accounts.get_or_create(&self.account_id);
6404 let dashboard = state
6405 .dashboards
6406 .get_mut(&existing.physical_id)
6407 .ok_or_else(|| format!("Dashboard {} not found", existing.physical_id))?;
6408 dashboard.size_bytes = body_str.len() as i64;
6409 dashboard.body = body_str;
6410 dashboard.last_modified = Utc::now();
6411 let arn = dashboard.arn.clone();
6412 Ok(ProvisionResult::new(existing.physical_id.clone()).with("Arn", arn))
6413 }
6414
6415 fn create_elbv2_load_balancer(
6418 &self,
6419 resource: &ResourceDefinition,
6420 ) -> Result<ProvisionResult, String> {
6421 let props = &resource.properties;
6422 let name = props
6423 .get("Name")
6424 .and_then(|v| v.as_str())
6425 .unwrap_or(&resource.logical_id)
6426 .to_string();
6427 let scheme = props
6428 .get("Scheme")
6429 .and_then(|v| v.as_str())
6430 .unwrap_or("internet-facing")
6431 .to_string();
6432 let lb_type = props
6433 .get("Type")
6434 .and_then(|v| v.as_str())
6435 .unwrap_or("application")
6436 .to_string();
6437 let ip_address_type = props
6438 .get("IpAddressType")
6439 .and_then(|v| v.as_str())
6440 .unwrap_or("ipv4")
6441 .to_string();
6442 let security_groups: Vec<String> = props
6443 .get("SecurityGroups")
6444 .and_then(|v| v.as_array())
6445 .map(|arr| {
6446 arr.iter()
6447 .filter_map(|s| s.as_str().map(|s| s.to_string()))
6448 .collect()
6449 })
6450 .unwrap_or_default();
6451 let tags = parse_elb_tags(props.get("Tags"));
6452
6453 let mut accounts = self.elbv2_state.write();
6454 let state = accounts.get_or_create(&self.account_id);
6455 let lb_id = Uuid::new_v4().simple().to_string();
6456 let arn = format!(
6457 "arn:aws:elasticloadbalancing:{}:{}:loadbalancer/{}/{}/{}",
6458 self.region,
6459 self.account_id,
6460 if lb_type == "network" { "net" } else { "app" },
6461 name,
6462 &lb_id[..16]
6463 );
6464 let dns_name = format!(
6465 "{}-{}.{}.elb.{}.amazonaws.com",
6466 name,
6467 &lb_id[..16],
6468 self.region,
6469 self.region
6470 );
6471
6472 let mut availability_zones: Vec<fakecloud_elbv2::AvailabilityZone> = Vec::new();
6473 if let Some(arr) = props.get("Subnets").and_then(|v| v.as_array()) {
6474 for s in arr {
6475 if let Some(subnet_id) = s.as_str() {
6476 availability_zones.push(fakecloud_elbv2::AvailabilityZone {
6477 zone_name: format!("{}a", self.region),
6478 subnet_id: subnet_id.to_string(),
6479 outpost_id: None,
6480 load_balancer_addresses: Vec::new(),
6481 source_nat_ipv6_prefixes: Vec::new(),
6482 });
6483 }
6484 }
6485 }
6486
6487 state.load_balancers.insert(
6488 arn.clone(),
6489 LoadBalancer {
6490 arn: arn.clone(),
6491 name: name.clone(),
6492 dns_name: dns_name.clone(),
6493 canonical_hosted_zone_id: "Z2P70J7EXAMPLE".to_string(),
6494 created_time: Utc::now(),
6495 scheme,
6496 vpc_id: String::new(),
6497 state_code: "active".to_string(),
6498 state_reason: None,
6499 lb_type,
6500 availability_zones,
6501 security_groups,
6502 ip_address_type,
6503 customer_owned_ipv4_pool: None,
6504 enforce_security_group_inbound_rules_on_private_link_traffic: None,
6505 enable_prefix_for_ipv6_source_nat: None,
6506 ipv4_ipam_pool_id: None,
6507 tags,
6508 attributes: BTreeMap::new(),
6509 minimum_capacity_units: None,
6510 bound_port: None,
6511 },
6512 );
6513
6514 Ok(ProvisionResult::new(arn.clone())
6515 .with("LoadBalancerArn", arn)
6516 .with(
6517 "LoadBalancerFullName",
6518 format!("app/{name}/{}", &lb_id[..16]),
6519 )
6520 .with("LoadBalancerName", name)
6521 .with("DNSName", dns_name)
6522 .with("CanonicalHostedZoneID", "Z2P70J7EXAMPLE"))
6523 }
6524
6525 fn delete_elbv2_load_balancer(&self, physical_id: &str) -> Result<(), String> {
6526 let mut accounts = self.elbv2_state.write();
6527 let state = accounts.get_or_create(&self.account_id);
6528 state.load_balancers.remove(physical_id);
6529 let listeners: Vec<String> = state
6531 .listeners
6532 .iter()
6533 .filter(|(_, l)| l.load_balancer_arn == physical_id)
6534 .map(|(arn, _)| arn.clone())
6535 .collect();
6536 for arn in &listeners {
6537 state.listeners.remove(arn);
6538 let rules: Vec<String> = state
6539 .rules
6540 .iter()
6541 .filter(|(_, r)| r.listener_arn == *arn)
6542 .map(|(a, _)| a.clone())
6543 .collect();
6544 for r in rules {
6545 state.rules.remove(&r);
6546 }
6547 }
6548 for tg in state.target_groups.values_mut() {
6549 tg.load_balancer_arns.retain(|a| a != physical_id);
6550 }
6551 Ok(())
6552 }
6553
6554 fn create_elbv2_target_group(
6555 &self,
6556 resource: &ResourceDefinition,
6557 ) -> Result<ProvisionResult, String> {
6558 let props = &resource.properties;
6559 let name = props
6560 .get("Name")
6561 .and_then(|v| v.as_str())
6562 .unwrap_or(&resource.logical_id)
6563 .to_string();
6564 let protocol = props
6565 .get("Protocol")
6566 .and_then(|v| v.as_str())
6567 .map(|s| s.to_string());
6568 let port = props.get("Port").and_then(|v| v.as_i64()).map(|n| n as i32);
6569 let vpc_id = props
6570 .get("VpcId")
6571 .and_then(|v| v.as_str())
6572 .map(|s| s.to_string());
6573 let target_type = props
6574 .get("TargetType")
6575 .and_then(|v| v.as_str())
6576 .unwrap_or("instance")
6577 .to_string();
6578 let ip_address_type = props
6579 .get("IpAddressType")
6580 .and_then(|v| v.as_str())
6581 .unwrap_or("ipv4")
6582 .to_string();
6583 let protocol_version = props
6584 .get("ProtocolVersion")
6585 .and_then(|v| v.as_str())
6586 .map(|s| s.to_string());
6587 let tags = parse_elb_tags(props.get("Tags"));
6588
6589 let mut accounts = self.elbv2_state.write();
6590 let state = accounts.get_or_create(&self.account_id);
6591 let id = Uuid::new_v4().simple().to_string();
6592 let arn = format!(
6593 "arn:aws:elasticloadbalancing:{}:{}:targetgroup/{}/{}",
6594 self.region,
6595 self.account_id,
6596 name,
6597 &id[..16]
6598 );
6599
6600 state.target_groups.insert(
6601 arn.clone(),
6602 TargetGroup {
6603 arn: arn.clone(),
6604 name: name.clone(),
6605 protocol,
6606 port,
6607 vpc_id,
6608 target_type,
6609 ip_address_type,
6610 protocol_version,
6611 health_check_protocol: props
6612 .get("HealthCheckProtocol")
6613 .and_then(|v| v.as_str())
6614 .map(|s| s.to_string()),
6615 health_check_port: props
6616 .get("HealthCheckPort")
6617 .and_then(|v| v.as_str())
6618 .map(|s| s.to_string()),
6619 health_check_enabled: props
6620 .get("HealthCheckEnabled")
6621 .and_then(|v| v.as_bool())
6622 .unwrap_or(true),
6623 health_check_path: props
6624 .get("HealthCheckPath")
6625 .and_then(|v| v.as_str())
6626 .map(|s| s.to_string()),
6627 health_check_interval_seconds: props
6628 .get("HealthCheckIntervalSeconds")
6629 .and_then(|v| v.as_i64())
6630 .unwrap_or(30) as i32,
6631 health_check_timeout_seconds: props
6632 .get("HealthCheckTimeoutSeconds")
6633 .and_then(|v| v.as_i64())
6634 .unwrap_or(5) as i32,
6635 healthy_threshold_count: props
6636 .get("HealthyThresholdCount")
6637 .and_then(|v| v.as_i64())
6638 .unwrap_or(5) as i32,
6639 unhealthy_threshold_count: props
6640 .get("UnhealthyThresholdCount")
6641 .and_then(|v| v.as_i64())
6642 .unwrap_or(2) as i32,
6643 matcher_http_code: props
6644 .get("Matcher")
6645 .and_then(|v| v.get("HttpCode"))
6646 .and_then(|v| v.as_str())
6647 .map(|s| s.to_string()),
6648 matcher_grpc_code: props
6649 .get("Matcher")
6650 .and_then(|v| v.get("GrpcCode"))
6651 .and_then(|v| v.as_str())
6652 .map(|s| s.to_string()),
6653 load_balancer_arns: Vec::new(),
6654 targets: Vec::new(),
6655 tags,
6656 attributes: BTreeMap::new(),
6657 created_time: Utc::now(),
6658 },
6659 );
6660
6661 Ok(ProvisionResult::new(arn.clone())
6662 .with("TargetGroupArn", arn)
6663 .with("TargetGroupName", name)
6664 .with("TargetGroupFullName", format!("targetgroup/{}", &id[..16])))
6665 }
6666
6667 fn delete_elbv2_target_group(&self, physical_id: &str) -> Result<(), String> {
6668 let mut accounts = self.elbv2_state.write();
6669 let state = accounts.get_or_create(&self.account_id);
6670 state.target_groups.remove(physical_id);
6671 Ok(())
6672 }
6673
6674 fn create_elbv2_listener(
6675 &self,
6676 resource: &ResourceDefinition,
6677 ) -> Result<ProvisionResult, String> {
6678 let props = &resource.properties;
6679 let load_balancer_arn = props
6680 .get("LoadBalancerArn")
6681 .and_then(|v| v.as_str())
6682 .ok_or_else(|| "LoadBalancerArn is required".to_string())?
6683 .to_string();
6684 let port = props.get("Port").and_then(|v| v.as_i64()).map(|n| n as i32);
6685 let protocol = props
6686 .get("Protocol")
6687 .and_then(|v| v.as_str())
6688 .map(|s| s.to_string());
6689 let default_actions = parse_elb_actions(props.get("DefaultActions"));
6690
6691 let mut accounts = self.elbv2_state.write();
6692 let state = accounts.get_or_create(&self.account_id);
6693 if !state.load_balancers.contains_key(&load_balancer_arn) {
6694 return Err(format!(
6695 "LoadBalancer {load_balancer_arn} not yet provisioned"
6696 ));
6697 }
6698
6699 let lb_full = load_balancer_arn
6700 .rsplit("loadbalancer/")
6701 .next()
6702 .unwrap_or("")
6703 .to_string();
6704 let listener_id = Uuid::new_v4().simple().to_string();
6705 let arn = format!(
6706 "arn:aws:elasticloadbalancing:{}:{}:listener/{}/{}",
6707 self.region,
6708 self.account_id,
6709 lb_full,
6710 &listener_id[..16]
6711 );
6712
6713 for action in &default_actions {
6716 if let Some(tg_arn) = &action.target_group_arn {
6717 if let Some(tg) = state.target_groups.get_mut(tg_arn) {
6718 if !tg.load_balancer_arns.contains(&load_balancer_arn) {
6719 tg.load_balancer_arns.push(load_balancer_arn.clone());
6720 }
6721 }
6722 }
6723 if let Some(forward) = &action.forward {
6724 for tgt in &forward.target_groups {
6725 if let Some(tg) = state.target_groups.get_mut(&tgt.target_group_arn) {
6726 if !tg.load_balancer_arns.contains(&load_balancer_arn) {
6727 tg.load_balancer_arns.push(load_balancer_arn.clone());
6728 }
6729 }
6730 }
6731 }
6732 }
6733
6734 state.listeners.insert(
6735 arn.clone(),
6736 Listener {
6737 arn: arn.clone(),
6738 load_balancer_arn,
6739 port,
6740 protocol,
6741 certificates: Vec::new(),
6742 ssl_policy: props
6743 .get("SslPolicy")
6744 .and_then(|v| v.as_str())
6745 .map(|s| s.to_string()),
6746 default_actions,
6747 alpn_policy: Vec::new(),
6748 mutual_authentication: None,
6749 tags: parse_elb_tags(props.get("Tags")),
6750 attributes: BTreeMap::new(),
6751 },
6752 );
6753
6754 Ok(ProvisionResult::new(arn.clone()).with("ListenerArn", arn))
6755 }
6756
6757 fn delete_elbv2_listener(&self, physical_id: &str) -> Result<(), String> {
6758 let mut accounts = self.elbv2_state.write();
6759 let state = accounts.get_or_create(&self.account_id);
6760 state.listeners.remove(physical_id);
6761 let rules: Vec<String> = state
6762 .rules
6763 .iter()
6764 .filter(|(_, r)| r.listener_arn == physical_id)
6765 .map(|(arn, _)| arn.clone())
6766 .collect();
6767 for r in rules {
6768 state.rules.remove(&r);
6769 }
6770 Ok(())
6771 }
6772
6773 fn create_elbv2_listener_rule(
6774 &self,
6775 resource: &ResourceDefinition,
6776 ) -> Result<ProvisionResult, String> {
6777 let props = &resource.properties;
6778 let listener_arn = props
6779 .get("ListenerArn")
6780 .and_then(|v| v.as_str())
6781 .ok_or_else(|| "ListenerArn is required".to_string())?
6782 .to_string();
6783 let priority = props
6784 .get("Priority")
6785 .map(|v| {
6786 if let Some(s) = v.as_str() {
6787 s.to_string()
6788 } else if let Some(n) = v.as_i64() {
6789 n.to_string()
6790 } else {
6791 "1".to_string()
6792 }
6793 })
6794 .unwrap_or_else(|| "1".to_string());
6795 let actions = parse_elb_actions(props.get("Actions"));
6796 let conditions = parse_elb_rule_conditions(props.get("Conditions"));
6797
6798 let mut accounts = self.elbv2_state.write();
6799 let state = accounts.get_or_create(&self.account_id);
6800 if !state.listeners.contains_key(&listener_arn) {
6801 return Err(format!("Listener {listener_arn} not yet provisioned"));
6802 }
6803 let listener_full = listener_arn
6804 .rsplit("listener/")
6805 .next()
6806 .unwrap_or("")
6807 .to_string();
6808 let rule_id = Uuid::new_v4().simple().to_string();
6809 let arn = format!(
6810 "arn:aws:elasticloadbalancing:{}:{}:listener-rule/{}/{}",
6811 self.region,
6812 self.account_id,
6813 listener_full,
6814 &rule_id[..16]
6815 );
6816
6817 state.rules.insert(
6818 arn.clone(),
6819 ElbRule {
6820 arn: arn.clone(),
6821 listener_arn,
6822 priority,
6823 conditions,
6824 actions,
6825 is_default: false,
6826 tags: parse_elb_tags(props.get("Tags")),
6827 },
6828 );
6829
6830 Ok(ProvisionResult::new(arn.clone()).with("RuleArn", arn))
6831 }
6832
6833 fn delete_elbv2_listener_rule(&self, physical_id: &str) -> Result<(), String> {
6834 let mut accounts = self.elbv2_state.write();
6835 let state = accounts.get_or_create(&self.account_id);
6836 state.rules.remove(physical_id);
6837 Ok(())
6838 }
6839
6840 fn create_elbv2_listener_certificate(
6845 &self,
6846 resource: &ResourceDefinition,
6847 ) -> Result<ProvisionResult, String> {
6848 let props = &resource.properties;
6849 let listener_arn = props
6850 .get("ListenerArn")
6851 .and_then(|v| v.as_str())
6852 .ok_or_else(|| "ListenerArn is required".to_string())?
6853 .to_string();
6854 let certs: Vec<String> = props
6855 .get("Certificates")
6856 .and_then(|v| v.as_array())
6857 .map(|arr| {
6858 arr.iter()
6859 .filter_map(|c| c.get("CertificateArn").and_then(|v| v.as_str()))
6860 .map(|s| s.to_string())
6861 .collect()
6862 })
6863 .unwrap_or_default();
6864 if certs.is_empty() {
6865 return Err("Certificates must contain at least one CertificateArn".to_string());
6866 }
6867 let mut accounts = self.elbv2_state.write();
6868 let state = accounts.get_or_create(&self.account_id);
6869 let listener = state
6870 .listeners
6871 .get_mut(&listener_arn)
6872 .ok_or_else(|| format!("Listener {listener_arn} does not exist"))?;
6873 for arn in &certs {
6874 listener.certificates.retain(|c| &c.certificate_arn != arn);
6875 listener.certificates.push(fakecloud_elbv2::Certificate {
6876 certificate_arn: arn.clone(),
6877 is_default: false,
6878 });
6879 }
6880 Ok(ProvisionResult::new(format!(
6881 "{}#{}",
6882 listener_arn,
6883 certs.join(",")
6884 )))
6885 }
6886
6887 fn delete_elbv2_listener_certificate(&self, physical_id: &str) -> Result<(), String> {
6888 let (listener_arn, cert_list) = match physical_id.split_once('#') {
6889 Some(parts) => parts,
6890 None => return Ok(()),
6891 };
6892 let cert_arns: Vec<&str> = cert_list.split(',').collect();
6893 let mut accounts = self.elbv2_state.write();
6894 let state = accounts.get_or_create(&self.account_id);
6895 if let Some(listener) = state.listeners.get_mut(listener_arn) {
6896 listener
6897 .certificates
6898 .retain(|c| !cert_arns.iter().any(|a| *a == c.certificate_arn));
6899 }
6900 Ok(())
6901 }
6902
6903 fn create_elbv2_trust_store(
6905 &self,
6906 resource: &ResourceDefinition,
6907 ) -> Result<ProvisionResult, String> {
6908 let props = &resource.properties;
6909 let name = props
6910 .get("Name")
6911 .and_then(|v| v.as_str())
6912 .unwrap_or(&resource.logical_id)
6913 .to_string();
6914 let bucket = props
6915 .get("CaCertificatesBundleS3Bucket")
6916 .and_then(|v| v.as_str())
6917 .ok_or_else(|| "CaCertificatesBundleS3Bucket is required".to_string())?;
6918 let key = props
6919 .get("CaCertificatesBundleS3Key")
6920 .and_then(|v| v.as_str())
6921 .ok_or_else(|| "CaCertificatesBundleS3Key is required".to_string())?;
6922 let tags: Vec<fakecloud_elbv2::Tag> = props
6923 .get("Tags")
6924 .and_then(|v| v.as_array())
6925 .map(|arr| {
6926 arr.iter()
6927 .filter_map(|t| {
6928 let k = t.get("Key").and_then(|v| v.as_str())?;
6929 let val = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
6930 Some(fakecloud_elbv2::Tag {
6931 key: k.to_string(),
6932 value: val.to_string(),
6933 })
6934 })
6935 .collect()
6936 })
6937 .unwrap_or_default();
6938
6939 let mut accounts = self.elbv2_state.write();
6940 let state = accounts.get_or_create(&self.account_id);
6941 if state.trust_stores.values().any(|t| t.name == name) {
6942 return Err(format!("Trust store {name} already exists"));
6943 }
6944 let suffix: String = Uuid::new_v4()
6945 .simple()
6946 .to_string()
6947 .chars()
6948 .take(16)
6949 .collect();
6950 let arn = format!(
6951 "arn:aws:elasticloadbalancing:{}:{}:truststore/{}/{}",
6952 self.region, self.account_id, name, suffix
6953 );
6954 let ts = fakecloud_elbv2::TrustStore {
6955 arn: arn.clone(),
6956 name: name.clone(),
6957 status: "ACTIVE".to_string(),
6958 number_of_ca_certificates: 1,
6959 total_revoked_entries: 0,
6960 created_time: Utc::now(),
6961 ca_certificates_bundle: Some(format!("s3://{bucket}/{key}").into_bytes()),
6962 revocations: BTreeMap::new(),
6963 next_revocation_id: 1,
6964 tags,
6965 };
6966 state.trust_stores.insert(arn.clone(), ts);
6967 Ok(ProvisionResult::new(arn.clone())
6968 .with("TrustStoreArn", arn)
6969 .with("Name", name)
6970 .with("Status", "ACTIVE".to_string()))
6971 }
6972
6973 fn delete_elbv2_trust_store(&self, physical_id: &str) -> Result<(), String> {
6974 let mut accounts = self.elbv2_state.write();
6975 let state = accounts.get_or_create(&self.account_id);
6976 state.trust_stores.remove(physical_id);
6977 Ok(())
6978 }
6979
6980 fn update_elbv2_load_balancer(
6985 &self,
6986 existing: &StackResource,
6987 resource: &ResourceDefinition,
6988 ) -> Result<ProvisionResult, String> {
6989 let props = &resource.properties;
6990 let arn = existing.physical_id.clone();
6991 let mut accounts = self.elbv2_state.write();
6992 let state = accounts.get_or_create(&self.account_id);
6993 let lb = state
6994 .load_balancers
6995 .get_mut(&arn)
6996 .ok_or_else(|| format!("LoadBalancer {arn} no longer exists"))?;
6997 if let Some(arr) = props.get("SecurityGroups").and_then(|v| v.as_array()) {
6998 lb.security_groups = arr
6999 .iter()
7000 .filter_map(|s| s.as_str().map(|s| s.to_string()))
7001 .collect();
7002 }
7003 if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
7004 lb.ip_address_type = s.to_string();
7005 }
7006 if let Some(arr) = props.get("Subnets").and_then(|v| v.as_array()) {
7007 let mut zones: Vec<fakecloud_elbv2::AvailabilityZone> = Vec::new();
7008 for s in arr {
7009 if let Some(subnet_id) = s.as_str() {
7010 zones.push(fakecloud_elbv2::AvailabilityZone {
7011 zone_name: format!("{}a", self.region),
7012 subnet_id: subnet_id.to_string(),
7013 outpost_id: None,
7014 load_balancer_addresses: Vec::new(),
7015 source_nat_ipv6_prefixes: Vec::new(),
7016 });
7017 }
7018 }
7019 lb.availability_zones = zones;
7020 }
7021 if props.get("Tags").is_some() {
7022 lb.tags = parse_elb_tags(props.get("Tags"));
7023 }
7024 let name = lb.name.clone();
7025 let dns_name = lb.dns_name.clone();
7026 let canonical = lb.canonical_hosted_zone_id.clone();
7027 let lb_full = arn.rsplit("loadbalancer/").next().unwrap_or("").to_string();
7028 Ok(ProvisionResult::new(arn.clone())
7029 .with("LoadBalancerArn", arn)
7030 .with("LoadBalancerFullName", lb_full)
7031 .with("LoadBalancerName", name)
7032 .with("DNSName", dns_name)
7033 .with("CanonicalHostedZoneID", canonical))
7034 }
7035
7036 fn update_elbv2_target_group(
7039 &self,
7040 existing: &StackResource,
7041 resource: &ResourceDefinition,
7042 ) -> Result<ProvisionResult, String> {
7043 let props = &resource.properties;
7044 let arn = existing.physical_id.clone();
7045 let mut accounts = self.elbv2_state.write();
7046 let state = accounts.get_or_create(&self.account_id);
7047 let tg = state
7048 .target_groups
7049 .get_mut(&arn)
7050 .ok_or_else(|| format!("TargetGroup {arn} no longer exists"))?;
7051 if let Some(s) = props.get("HealthCheckProtocol").and_then(|v| v.as_str()) {
7052 tg.health_check_protocol = Some(s.to_string());
7053 }
7054 if let Some(s) = props.get("HealthCheckPort").and_then(|v| v.as_str()) {
7055 tg.health_check_port = Some(s.to_string());
7056 }
7057 if let Some(b) = props.get("HealthCheckEnabled").and_then(|v| v.as_bool()) {
7058 tg.health_check_enabled = b;
7059 }
7060 if let Some(s) = props.get("HealthCheckPath").and_then(|v| v.as_str()) {
7061 tg.health_check_path = Some(s.to_string());
7062 }
7063 if let Some(n) = props.get("HealthCheckIntervalSeconds").and_then(cfn_as_i64) {
7064 tg.health_check_interval_seconds = n as i32;
7065 }
7066 if let Some(n) = props.get("HealthCheckTimeoutSeconds").and_then(cfn_as_i64) {
7067 tg.health_check_timeout_seconds = n as i32;
7068 }
7069 if let Some(n) = props.get("HealthyThresholdCount").and_then(cfn_as_i64) {
7070 tg.healthy_threshold_count = n as i32;
7071 }
7072 if let Some(n) = props.get("UnhealthyThresholdCount").and_then(cfn_as_i64) {
7073 tg.unhealthy_threshold_count = n as i32;
7074 }
7075 if let Some(matcher) = props.get("Matcher") {
7076 tg.matcher_http_code = matcher
7077 .get("HttpCode")
7078 .and_then(|v| v.as_str())
7079 .map(|s| s.to_string());
7080 tg.matcher_grpc_code = matcher
7081 .get("GrpcCode")
7082 .and_then(|v| v.as_str())
7083 .map(|s| s.to_string());
7084 }
7085 if props.get("Tags").is_some() {
7086 tg.tags = parse_elb_tags(props.get("Tags"));
7087 }
7088 let name = tg.name.clone();
7089 let tg_full = arn
7090 .rsplit("targetgroup/")
7091 .next()
7092 .map(|s| format!("targetgroup/{s}"))
7093 .unwrap_or_default();
7094 Ok(ProvisionResult::new(arn.clone())
7095 .with("TargetGroupArn", arn)
7096 .with("TargetGroupName", name)
7097 .with("TargetGroupFullName", tg_full))
7098 }
7099
7100 fn update_elbv2_listener(
7103 &self,
7104 existing: &StackResource,
7105 resource: &ResourceDefinition,
7106 ) -> Result<ProvisionResult, String> {
7107 let props = &resource.properties;
7108 let arn = existing.physical_id.clone();
7109 let new_default_actions = props
7110 .get("DefaultActions")
7111 .map(|v| parse_elb_actions(Some(v)));
7112 let mut accounts = self.elbv2_state.write();
7113 let state = accounts.get_or_create(&self.account_id);
7114 let listener = state
7115 .listeners
7116 .get_mut(&arn)
7117 .ok_or_else(|| format!("Listener {arn} no longer exists"))?;
7118 if let Some(n) = props.get("Port").and_then(cfn_as_i64) {
7119 listener.port = Some(n as i32);
7120 }
7121 if let Some(s) = props.get("Protocol").and_then(|v| v.as_str()) {
7122 listener.protocol = Some(s.to_string());
7123 }
7124 if let Some(s) = props.get("SslPolicy").and_then(|v| v.as_str()) {
7125 listener.ssl_policy = Some(s.to_string());
7126 }
7127 if let Some(actions) = new_default_actions {
7128 listener.default_actions = actions;
7129 }
7130 if props.get("Tags").is_some() {
7131 listener.tags = parse_elb_tags(props.get("Tags"));
7132 }
7133 Ok(ProvisionResult::new(arn.clone()).with("ListenerArn", arn))
7134 }
7135
7136 fn update_elbv2_listener_rule(
7139 &self,
7140 existing: &StackResource,
7141 resource: &ResourceDefinition,
7142 ) -> Result<ProvisionResult, String> {
7143 let props = &resource.properties;
7144 let arn = existing.physical_id.clone();
7145 let new_actions = props.get("Actions").map(|v| parse_elb_actions(Some(v)));
7146 let new_conditions = props
7147 .get("Conditions")
7148 .map(|v| parse_elb_rule_conditions(Some(v)));
7149 let mut accounts = self.elbv2_state.write();
7150 let state = accounts.get_or_create(&self.account_id);
7151 let rule = state
7152 .rules
7153 .get_mut(&arn)
7154 .ok_or_else(|| format!("ListenerRule {arn} no longer exists"))?;
7155 if let Some(v) = props.get("Priority") {
7156 rule.priority = if let Some(s) = v.as_str() {
7157 s.to_string()
7158 } else if let Some(n) = v.as_i64() {
7159 n.to_string()
7160 } else {
7161 rule.priority.clone()
7162 };
7163 }
7164 if let Some(actions) = new_actions {
7165 rule.actions = actions;
7166 }
7167 if let Some(conditions) = new_conditions {
7168 rule.conditions = conditions;
7169 }
7170 if props.get("Tags").is_some() {
7171 rule.tags = parse_elb_tags(props.get("Tags"));
7172 }
7173 Ok(ProvisionResult::new(arn.clone()).with("RuleArn", arn))
7174 }
7175
7176 fn update_elbv2_listener_certificate(
7181 &self,
7182 existing: &StackResource,
7183 resource: &ResourceDefinition,
7184 ) -> Result<ProvisionResult, String> {
7185 let props = &resource.properties;
7186 let physical_id = existing.physical_id.clone();
7187 let listener_arn = props
7188 .get("ListenerArn")
7189 .and_then(|v| v.as_str())
7190 .map(|s| s.to_string())
7191 .or_else(|| physical_id.split_once('#').map(|(l, _)| l.to_string()))
7192 .ok_or_else(|| "ListenerArn is required".to_string())?;
7193 let new_certs: Vec<String> = props
7194 .get("Certificates")
7195 .and_then(|v| v.as_array())
7196 .map(|arr| {
7197 arr.iter()
7198 .filter_map(|c| c.get("CertificateArn").and_then(|v| v.as_str()))
7199 .map(|s| s.to_string())
7200 .collect()
7201 })
7202 .unwrap_or_default();
7203 if new_certs.is_empty() {
7204 return Err("Certificates must contain at least one CertificateArn".to_string());
7205 }
7206
7207 let prev_certs: Vec<String> = physical_id
7209 .split_once('#')
7210 .map(|(_, list)| list.split(',').map(|s| s.to_string()).collect())
7211 .unwrap_or_default();
7212
7213 let mut accounts = self.elbv2_state.write();
7214 let state = accounts.get_or_create(&self.account_id);
7215 let listener = state
7216 .listeners
7217 .get_mut(&listener_arn)
7218 .ok_or_else(|| format!("Listener {listener_arn} does not exist"))?;
7219 listener
7220 .certificates
7221 .retain(|c| !prev_certs.iter().any(|p| p == &c.certificate_arn));
7222 for arn in &new_certs {
7223 listener.certificates.retain(|c| &c.certificate_arn != arn);
7224 listener.certificates.push(fakecloud_elbv2::Certificate {
7225 certificate_arn: arn.clone(),
7226 is_default: false,
7227 });
7228 }
7229 Ok(ProvisionResult::new(format!(
7230 "{}#{}",
7231 listener_arn,
7232 new_certs.join(",")
7233 )))
7234 }
7235
7236 fn update_elbv2_trust_store(
7239 &self,
7240 existing: &StackResource,
7241 resource: &ResourceDefinition,
7242 ) -> Result<ProvisionResult, String> {
7243 let props = &resource.properties;
7244 let arn = existing.physical_id.clone();
7245 let mut accounts = self.elbv2_state.write();
7246 let state = accounts.get_or_create(&self.account_id);
7247 let ts = state
7248 .trust_stores
7249 .get_mut(&arn)
7250 .ok_or_else(|| format!("TrustStore {arn} no longer exists"))?;
7251 let new_bucket = props
7252 .get("CaCertificatesBundleS3Bucket")
7253 .and_then(|v| v.as_str());
7254 let new_key = props
7255 .get("CaCertificatesBundleS3Key")
7256 .and_then(|v| v.as_str());
7257 if let (Some(b), Some(k)) = (new_bucket, new_key) {
7258 ts.ca_certificates_bundle = Some(format!("s3://{b}/{k}").into_bytes());
7259 }
7260 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
7261 ts.tags = arr
7262 .iter()
7263 .filter_map(|t| {
7264 let k = t.get("Key").and_then(|v| v.as_str())?;
7265 let v = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
7266 Some(fakecloud_elbv2::Tag {
7267 key: k.to_string(),
7268 value: v.to_string(),
7269 })
7270 })
7271 .collect();
7272 }
7273 let name = ts.name.clone();
7274 let status = ts.status.clone();
7275 Ok(ProvisionResult::new(arn.clone())
7276 .with("TrustStoreArn", arn)
7277 .with("Name", name)
7278 .with("Status", status))
7279 }
7280
7281 fn get_att_elbv2_load_balancer(&self, physical_id: &str, attribute: &str) -> Option<String> {
7283 let mut accounts = self.elbv2_state.write();
7284 let state = accounts.get_or_create(&self.account_id);
7285 let lb = state.load_balancers.get(physical_id)?;
7286 let lb_full = lb
7287 .arn
7288 .rsplit("loadbalancer/")
7289 .next()
7290 .unwrap_or("")
7291 .to_string();
7292 match attribute {
7293 "Arn" | "LoadBalancerArn" => Some(lb.arn.clone()),
7294 "DNSName" => Some(lb.dns_name.clone()),
7295 "CanonicalHostedZoneID" => Some(lb.canonical_hosted_zone_id.clone()),
7296 "LoadBalancerFullName" => Some(lb_full),
7297 "LoadBalancerName" => Some(lb.name.clone()),
7298 "SecurityGroups" => Some(lb.security_groups.join(",")),
7299 _ => None,
7300 }
7301 }
7302
7303 fn get_att_elbv2_target_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
7305 let mut accounts = self.elbv2_state.write();
7306 let state = accounts.get_or_create(&self.account_id);
7307 let tg = state.target_groups.get(physical_id)?;
7308 let tg_full = tg
7309 .arn
7310 .rsplit("targetgroup/")
7311 .next()
7312 .map(|s| format!("targetgroup/{s}"))
7313 .unwrap_or_default();
7314 match attribute {
7315 "TargetGroupArn" => Some(tg.arn.clone()),
7316 "TargetGroupName" => Some(tg.name.clone()),
7317 "TargetGroupFullName" => Some(tg_full),
7318 "LoadBalancerArns" => Some(tg.load_balancer_arns.join(",")),
7319 _ => None,
7320 }
7321 }
7322
7323 fn get_att_elbv2_listener(&self, physical_id: &str, attribute: &str) -> Option<String> {
7325 let mut accounts = self.elbv2_state.write();
7326 let state = accounts.get_or_create(&self.account_id);
7327 let listener = state.listeners.get(physical_id)?;
7328 match attribute {
7329 "Arn" | "ListenerArn" => Some(listener.arn.clone()),
7330 _ => None,
7331 }
7332 }
7333
7334 fn get_att_elbv2_listener_rule(&self, physical_id: &str, attribute: &str) -> Option<String> {
7336 let mut accounts = self.elbv2_state.write();
7337 let state = accounts.get_or_create(&self.account_id);
7338 let rule = state.rules.get(physical_id)?;
7339 match attribute {
7340 "RuleArn" => Some(rule.arn.clone()),
7341 "IsDefault" => Some(rule.is_default.to_string()),
7342 _ => None,
7343 }
7344 }
7345
7346 fn get_att_elbv2_trust_store(&self, physical_id: &str, attribute: &str) -> Option<String> {
7348 let mut accounts = self.elbv2_state.write();
7349 let state = accounts.get_or_create(&self.account_id);
7350 let ts = state.trust_stores.get(physical_id)?;
7351 match attribute {
7352 "TrustStoreArn" => Some(ts.arn.clone()),
7353 "Name" => Some(ts.name.clone()),
7354 "Status" => Some(ts.status.clone()),
7355 "NumberOfCaCertificates" => Some(ts.number_of_ca_certificates.to_string()),
7356 "TotalRevokedEntries" => Some(ts.total_revoked_entries.to_string()),
7357 _ => None,
7358 }
7359 }
7360
7361 fn create_organization(
7364 &self,
7365 resource: &ResourceDefinition,
7366 ) -> Result<ProvisionResult, String> {
7367 let props = &resource.properties;
7368 let feature_set = props
7369 .get("FeatureSet")
7370 .and_then(|v| v.as_str())
7371 .unwrap_or("ALL")
7372 .to_string();
7373
7374 let mut org = self.organizations_state.write();
7375 if org.is_some() {
7376 return Err("Organization already exists; only one per fakecloud process".to_string());
7377 }
7378 let mut state = OrganizationState::bootstrap(&self.account_id);
7379 state.feature_set = feature_set;
7380 let org_id = state.org_id.clone();
7381 let org_arn = state.org_arn.clone();
7382 let mgmt_arn = state.management_account_arn.clone();
7383 let root_id = state.root_id.clone();
7384 *org = Some(state);
7385
7386 Ok(ProvisionResult::new(org_id.clone())
7387 .with("Id", org_id)
7388 .with("Arn", org_arn)
7389 .with("ManagementAccountArn", mgmt_arn)
7390 .with("RootId", root_id))
7391 }
7392
7393 fn delete_organization(&self, _physical_id: &str) -> Result<(), String> {
7394 let mut org = self.organizations_state.write();
7395 *org = None;
7396 Ok(())
7397 }
7398
7399 fn create_organization_unit(
7400 &self,
7401 resource: &ResourceDefinition,
7402 ) -> Result<ProvisionResult, String> {
7403 let props = &resource.properties;
7404 let name = props
7405 .get("Name")
7406 .and_then(|v| v.as_str())
7407 .unwrap_or(&resource.logical_id)
7408 .to_string();
7409 let parent_id = props
7410 .get("ParentId")
7411 .and_then(|v| v.as_str())
7412 .ok_or_else(|| "ParentId is required".to_string())?
7413 .to_string();
7414
7415 let mut org_lock = self.organizations_state.write();
7416 let org = org_lock
7417 .as_mut()
7418 .ok_or_else(|| "Organization not yet created".to_string())?;
7419 let resolved_parent_id = if parent_id == org.root_id || org.ous.contains_key(&parent_id) {
7421 parent_id
7422 } else {
7423 return Err(format!("Parent {parent_id} does not exist"));
7424 };
7425 let id_suffix: String = Uuid::new_v4()
7426 .simple()
7427 .to_string()
7428 .chars()
7429 .take(8)
7430 .collect();
7431 let id = format!("ou-{}-{}", &org.root_id[2..], id_suffix);
7432 let arn = format!(
7433 "arn:aws:organizations::{}:ou/{}/{}",
7434 org.management_account_id, org.org_id, id
7435 );
7436 org.ous.insert(
7437 id.clone(),
7438 OrganizationalUnit {
7439 id: id.clone(),
7440 arn: arn.clone(),
7441 name: name.clone(),
7442 parent_id: resolved_parent_id,
7443 },
7444 );
7445 Ok(ProvisionResult::new(id.clone())
7446 .with("Id", id)
7447 .with("Arn", arn)
7448 .with("Name", name))
7449 }
7450
7451 fn delete_organization_unit(&self, physical_id: &str) -> Result<(), String> {
7452 let mut org_lock = self.organizations_state.write();
7453 if let Some(org) = org_lock.as_mut() {
7454 org.ous.remove(physical_id);
7455 org.attachments.remove(physical_id);
7456 }
7457 Ok(())
7458 }
7459
7460 fn create_organization_account(
7464 &self,
7465 resource: &ResourceDefinition,
7466 ) -> Result<ProvisionResult, String> {
7467 let props = &resource.properties;
7468 let email = props
7469 .get("Email")
7470 .and_then(|v| v.as_str())
7471 .ok_or_else(|| "Email is required".to_string())?
7472 .to_string();
7473 let name = props
7474 .get("AccountName")
7475 .and_then(|v| v.as_str())
7476 .ok_or_else(|| "AccountName is required".to_string())?
7477 .to_string();
7478 let parent_ids: Vec<String> = props
7479 .get("ParentIds")
7480 .and_then(|v| v.as_array())
7481 .map(|arr| {
7482 arr.iter()
7483 .filter_map(|v| v.as_str().map(|s| s.to_string()))
7484 .collect()
7485 })
7486 .unwrap_or_default();
7487 let tags: Vec<(String, String)> = props
7488 .get("Tags")
7489 .and_then(|v| v.as_array())
7490 .map(|arr| {
7491 arr.iter()
7492 .filter_map(|t| {
7493 let k = t.get("Key").and_then(|v| v.as_str())?;
7494 let val = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
7495 Some((k.to_string(), val.to_string()))
7496 })
7497 .collect()
7498 })
7499 .unwrap_or_default();
7500
7501 let mut org_lock = self.organizations_state.write();
7502 let org = org_lock
7503 .as_mut()
7504 .ok_or_else(|| "Organization not yet created".to_string())?;
7505 let pending = org.begin_create_account(&email, &name, None);
7510 let status = org.complete_create_account(&pending.id).unwrap_or(pending);
7511 let account_id = status
7512 .account_id
7513 .clone()
7514 .ok_or_else(|| "create_account did not return an account id".to_string())?;
7515 let account_arn = org
7516 .accounts
7517 .get(&account_id)
7518 .map(|a| a.arn.clone())
7519 .unwrap_or_default();
7520 let joined_method = org
7521 .accounts
7522 .get(&account_id)
7523 .map(|a| a.joined_method.clone())
7524 .unwrap_or_else(|| "CREATED".to_string());
7525 let joined_timestamp = org
7526 .accounts
7527 .get(&account_id)
7528 .map(|a| a.joined_timestamp.to_rfc3339())
7529 .unwrap_or_default();
7530 let acct_status = org
7531 .accounts
7532 .get(&account_id)
7533 .map(|a| a.status.clone())
7534 .unwrap_or_else(|| "ACTIVE".to_string());
7535
7536 if let Some(parent) = parent_ids.first() {
7537 let source = org
7538 .accounts
7539 .get(&account_id)
7540 .map(|a| a.parent_id.clone())
7541 .unwrap_or_else(|| org.root_id.clone());
7542 if parent != &source {
7543 org.move_account(&account_id, &source, parent)
7544 .map_err(|e| format!("Failed to move account to parent {parent}: {e:?}"))?;
7545 }
7546 }
7547
7548 if !tags.is_empty() {
7549 org.set_resource_tags(&account_id, &tags);
7550 }
7551
7552 Ok(ProvisionResult::new(account_id.clone())
7553 .with("AccountId", account_id)
7554 .with("AccountName", name)
7555 .with("Email", email)
7556 .with("Arn", account_arn)
7557 .with("JoinedMethod", joined_method)
7558 .with("JoinedTimestamp", joined_timestamp)
7559 .with("Status", acct_status))
7560 }
7561
7562 fn delete_organization_account(&self, physical_id: &str) -> Result<(), String> {
7566 let mut org_lock = self.organizations_state.write();
7567 if let Some(org) = org_lock.as_mut() {
7568 let _ = org.close_account(physical_id);
7569 }
7570 Ok(())
7571 }
7572
7573 fn create_organization_policy(
7574 &self,
7575 resource: &ResourceDefinition,
7576 ) -> Result<ProvisionResult, String> {
7577 let props = &resource.properties;
7578 let name = props
7579 .get("Name")
7580 .and_then(|v| v.as_str())
7581 .unwrap_or(&resource.logical_id)
7582 .to_string();
7583 let description = props
7584 .get("Description")
7585 .and_then(|v| v.as_str())
7586 .unwrap_or("")
7587 .to_string();
7588 let policy_type = props
7589 .get("Type")
7590 .and_then(|v| v.as_str())
7591 .unwrap_or(POLICY_TYPE_SCP)
7592 .to_string();
7593 let content = props
7594 .get("Content")
7595 .map(|v| {
7596 if v.is_string() {
7597 v.as_str().unwrap_or("").to_string()
7598 } else {
7599 serde_json::to_string(v).unwrap_or_default()
7600 }
7601 })
7602 .unwrap_or_default();
7603 let target_ids: Vec<String> = props
7604 .get("TargetIds")
7605 .and_then(|v| v.as_array())
7606 .map(|arr| {
7607 arr.iter()
7608 .filter_map(|t| t.as_str().map(|s| s.to_string()))
7609 .collect()
7610 })
7611 .unwrap_or_default();
7612
7613 let mut org_lock = self.organizations_state.write();
7614 let org = org_lock
7615 .as_mut()
7616 .ok_or_else(|| "Organization not yet created".to_string())?;
7617 let id_suffix: String = Uuid::new_v4()
7618 .simple()
7619 .to_string()
7620 .chars()
7621 .take(8)
7622 .collect();
7623 let id = format!("p-{}", id_suffix);
7624 let arn = format!(
7625 "arn:aws:organizations::{}:policy/{}/{}/{}",
7626 org.management_account_id,
7627 org.org_id,
7628 policy_type.to_lowercase(),
7629 id
7630 );
7631 org.policies.insert(
7632 id.clone(),
7633 OrgPolicy {
7634 id: id.clone(),
7635 arn: arn.clone(),
7636 name: name.clone(),
7637 description,
7638 policy_type,
7639 aws_managed: false,
7640 content,
7641 },
7642 );
7643 for target in target_ids {
7644 org.attachments
7645 .entry(target)
7646 .or_default()
7647 .insert(id.clone());
7648 }
7649 Ok(ProvisionResult::new(id.clone())
7650 .with("Id", id)
7651 .with("Arn", arn)
7652 .with("Name", name))
7653 }
7654
7655 fn delete_organization_policy(&self, physical_id: &str) -> Result<(), String> {
7656 let mut org_lock = self.organizations_state.write();
7657 if let Some(org) = org_lock.as_mut() {
7658 org.policies.remove(physical_id);
7659 for attachments in org.attachments.values_mut() {
7660 attachments.remove(physical_id);
7661 }
7662 }
7663 Ok(())
7664 }
7665
7666 fn create_organization_resource_policy(
7667 &self,
7668 resource: &ResourceDefinition,
7669 ) -> Result<ProvisionResult, String> {
7670 let props = &resource.properties;
7671 let content = props
7672 .get("Content")
7673 .map(|v| {
7674 if v.is_string() {
7675 v.as_str().unwrap_or("").to_string()
7676 } else {
7677 serde_json::to_string(v).unwrap_or_default()
7678 }
7679 })
7680 .ok_or_else(|| "Content is required".to_string())?;
7681
7682 let mut org_lock = self.organizations_state.write();
7683 let org = org_lock
7684 .as_mut()
7685 .ok_or_else(|| "Organization not yet created".to_string())?;
7686 org.resource_policy = Some(content);
7687 let arn = format!(
7688 "arn:aws:organizations::{}:resourcepolicy/{}/rp",
7689 org.management_account_id, org.org_id
7690 );
7691 Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
7692 }
7693
7694 fn delete_organization_resource_policy(&self, _physical_id: &str) -> Result<(), String> {
7695 let mut org_lock = self.organizations_state.write();
7696 if let Some(org) = org_lock.as_mut() {
7697 org.resource_policy = None;
7698 }
7699 Ok(())
7700 }
7701
7702 fn delete_log_group(&self, physical_id: &str) -> Result<(), String> {
7703 let mut logs_accounts = self.logs_state.write();
7704 let state = logs_accounts.default_mut();
7705 let name = state
7707 .log_groups
7708 .iter()
7709 .find(|(_, g)| g.arn == physical_id)
7710 .map(|(name, _)| name.clone());
7711 if let Some(name) = name {
7712 state.log_groups.remove(&name);
7713 }
7714 Ok(())
7715 }
7716
7717 fn create_log_stream(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
7718 let props = &resource.properties;
7719 let log_group_name = props
7720 .get("LogGroupName")
7721 .and_then(|v| v.as_str())
7722 .map(parse_log_group_name)
7723 .ok_or_else(|| "LogGroupName is required".to_string())?;
7724 let log_stream_name = props
7725 .get("LogStreamName")
7726 .and_then(|v| v.as_str())
7727 .unwrap_or(&resource.logical_id)
7728 .to_string();
7729
7730 let mut logs_accounts = self.logs_state.write();
7731 let state = logs_accounts.get_or_create(&self.account_id);
7732 let group = state
7733 .log_groups
7734 .get_mut(&log_group_name)
7735 .ok_or_else(|| format!("Log group {log_group_name} does not exist"))?;
7736 let arn = format!(
7737 "arn:aws:logs:{}:{}:log-group:{}:log-stream:{}",
7738 self.region, self.account_id, log_group_name, log_stream_name
7739 );
7740 if group.log_streams.contains_key(&log_stream_name) {
7741 return Err(format!(
7742 "Log stream {log_stream_name} already exists in {log_group_name}"
7743 ));
7744 }
7745 group.log_streams.insert(
7746 log_stream_name.clone(),
7747 LogStream {
7748 name: log_stream_name.clone(),
7749 arn,
7750 creation_time: Utc::now().timestamp_millis(),
7751 first_event_timestamp: None,
7752 last_event_timestamp: None,
7753 last_ingestion_time: None,
7754 upload_sequence_token: String::new(),
7755 events: Vec::new(),
7756 },
7757 );
7758
7759 let physical_id = format!("{log_group_name}|{log_stream_name}");
7761 Ok(ProvisionResult::new(physical_id))
7762 }
7763
7764 fn delete_log_stream(&self, physical_id: &str) -> Result<(), String> {
7765 let mut logs_accounts = self.logs_state.write();
7766 let state = logs_accounts.get_or_create(&self.account_id);
7767 if let Some((group_name, stream_name)) = physical_id.split_once('|') {
7768 if let Some(group) = state.log_groups.get_mut(group_name) {
7769 group.log_streams.remove(stream_name);
7770 }
7771 }
7772 Ok(())
7773 }
7774
7775 fn create_metric_filter(
7776 &self,
7777 resource: &ResourceDefinition,
7778 ) -> Result<ProvisionResult, String> {
7779 let props = &resource.properties;
7780 let log_group_name = props
7781 .get("LogGroupName")
7782 .and_then(|v| v.as_str())
7783 .map(parse_log_group_name)
7784 .ok_or_else(|| "LogGroupName is required".to_string())?;
7785 let filter_name = props
7786 .get("FilterName")
7787 .and_then(|v| v.as_str())
7788 .unwrap_or(&resource.logical_id)
7789 .to_string();
7790 let filter_pattern = props
7791 .get("FilterPattern")
7792 .and_then(|v| v.as_str())
7793 .unwrap_or("")
7794 .to_string();
7795
7796 let mut transformations: Vec<MetricTransformation> = Vec::new();
7797 if let Some(arr) = props
7798 .get("MetricTransformations")
7799 .and_then(|v| v.as_array())
7800 {
7801 for t in arr {
7802 let metric_name = t
7803 .get("MetricName")
7804 .and_then(|v| v.as_str())
7805 .unwrap_or("")
7806 .to_string();
7807 let metric_namespace = t
7808 .get("MetricNamespace")
7809 .and_then(|v| v.as_str())
7810 .unwrap_or("")
7811 .to_string();
7812 let metric_value = t
7813 .get("MetricValue")
7814 .and_then(|v| v.as_str())
7815 .unwrap_or("1")
7816 .to_string();
7817 let default_value = t.get("DefaultValue").and_then(|v| v.as_f64());
7818 transformations.push(MetricTransformation {
7819 metric_name,
7820 metric_namespace,
7821 metric_value,
7822 default_value,
7823 });
7824 }
7825 }
7826
7827 let mut logs_accounts = self.logs_state.write();
7828 let state = logs_accounts.get_or_create(&self.account_id);
7829 if !state.log_groups.contains_key(&log_group_name) {
7830 return Err(format!("Log group {log_group_name} does not exist"));
7831 }
7832 state
7833 .metric_filters
7834 .retain(|f| !(f.log_group_name == log_group_name && f.filter_name == filter_name));
7835 state.metric_filters.push(MetricFilter {
7836 filter_name: filter_name.clone(),
7837 filter_pattern,
7838 log_group_name: log_group_name.clone(),
7839 metric_transformations: transformations,
7840 creation_time: Utc::now().timestamp_millis(),
7841 });
7842
7843 Ok(ProvisionResult::new(format!(
7844 "{log_group_name}|{filter_name}"
7845 )))
7846 }
7847
7848 fn delete_metric_filter(&self, physical_id: &str) -> Result<(), String> {
7849 let mut logs_accounts = self.logs_state.write();
7850 let state = logs_accounts.get_or_create(&self.account_id);
7851 if let Some((group_name, filter_name)) = physical_id.split_once('|') {
7852 state
7853 .metric_filters
7854 .retain(|f| !(f.log_group_name == group_name && f.filter_name == filter_name));
7855 }
7856 Ok(())
7857 }
7858
7859 fn create_subscription_filter(
7860 &self,
7861 resource: &ResourceDefinition,
7862 ) -> Result<ProvisionResult, String> {
7863 let props = &resource.properties;
7864 let log_group_name = props
7865 .get("LogGroupName")
7866 .and_then(|v| v.as_str())
7867 .map(parse_log_group_name)
7868 .ok_or_else(|| "LogGroupName is required".to_string())?;
7869 let filter_name = props
7870 .get("FilterName")
7871 .and_then(|v| v.as_str())
7872 .unwrap_or(&resource.logical_id)
7873 .to_string();
7874 let filter_pattern = props
7875 .get("FilterPattern")
7876 .and_then(|v| v.as_str())
7877 .unwrap_or("")
7878 .to_string();
7879 let destination_arn = props
7880 .get("DestinationArn")
7881 .and_then(|v| v.as_str())
7882 .ok_or_else(|| "DestinationArn is required".to_string())?
7883 .to_string();
7884 let role_arn = props
7885 .get("RoleArn")
7886 .and_then(|v| v.as_str())
7887 .map(|s| s.to_string());
7888 let distribution = props
7889 .get("Distribution")
7890 .and_then(|v| v.as_str())
7891 .unwrap_or("ByLogStream")
7892 .to_string();
7893
7894 let mut logs_accounts = self.logs_state.write();
7895 let state = logs_accounts.get_or_create(&self.account_id);
7896 let group = state
7897 .log_groups
7898 .get_mut(&log_group_name)
7899 .ok_or_else(|| format!("Log group {log_group_name} does not exist"))?;
7900 group
7901 .subscription_filters
7902 .retain(|f| f.filter_name != filter_name);
7903 group.subscription_filters.push(SubscriptionFilter {
7904 filter_name: filter_name.clone(),
7905 log_group_name: log_group_name.clone(),
7906 filter_pattern,
7907 destination_arn,
7908 role_arn,
7909 distribution,
7910 creation_time: Utc::now().timestamp_millis(),
7911 });
7912
7913 Ok(ProvisionResult::new(format!(
7914 "{log_group_name}|{filter_name}"
7915 )))
7916 }
7917
7918 fn delete_subscription_filter(&self, physical_id: &str) -> Result<(), String> {
7919 let mut logs_accounts = self.logs_state.write();
7920 let state = logs_accounts.get_or_create(&self.account_id);
7921 if let Some((group_name, filter_name)) = physical_id.split_once('|') {
7922 if let Some(group) = state.log_groups.get_mut(group_name) {
7923 group
7924 .subscription_filters
7925 .retain(|f| f.filter_name != filter_name);
7926 }
7927 }
7928 Ok(())
7929 }
7930
7931 fn create_logs_destination(
7934 &self,
7935 resource: &ResourceDefinition,
7936 ) -> Result<ProvisionResult, String> {
7937 let props = &resource.properties;
7938 let destination_name = props
7939 .get("DestinationName")
7940 .and_then(|v| v.as_str())
7941 .ok_or("DestinationName is required")?
7942 .to_string();
7943 let target_arn = props
7944 .get("TargetArn")
7945 .and_then(|v| v.as_str())
7946 .ok_or("TargetArn is required")?
7947 .to_string();
7948 let role_arn = props
7949 .get("RoleArn")
7950 .and_then(|v| v.as_str())
7951 .ok_or("RoleArn is required")?
7952 .to_string();
7953 let access_policy = props
7954 .get("DestinationPolicy")
7955 .and_then(|v| v.as_str())
7956 .map(String::from);
7957
7958 let arn = format!(
7959 "arn:aws:logs:{}:{}:destination:{destination_name}",
7960 self.region, self.account_id
7961 );
7962 let dest = Destination {
7963 destination_name: destination_name.clone(),
7964 target_arn,
7965 role_arn,
7966 arn: arn.clone(),
7967 access_policy,
7968 creation_time: Utc::now().timestamp_millis(),
7969 tags: BTreeMap::new(),
7970 };
7971
7972 let mut logs_accounts = self.logs_state.write();
7973 let state = logs_accounts.get_or_create(&self.account_id);
7974 state.destinations.insert(destination_name.clone(), dest);
7975
7976 Ok(ProvisionResult::new(destination_name).with("Arn", arn))
7977 }
7978
7979 fn delete_logs_destination(&self, physical_id: &str) -> Result<(), String> {
7980 let mut logs_accounts = self.logs_state.write();
7981 let state = logs_accounts.get_or_create(&self.account_id);
7982 state.destinations.remove(physical_id);
7983 Ok(())
7984 }
7985
7986 fn create_logs_resource_policy(
7987 &self,
7988 resource: &ResourceDefinition,
7989 ) -> Result<ProvisionResult, String> {
7990 let props = &resource.properties;
7991 let policy_name = props
7992 .get("PolicyName")
7993 .and_then(|v| v.as_str())
7994 .ok_or("PolicyName is required")?
7995 .to_string();
7996 let policy_document = props
7997 .get("PolicyDocument")
7998 .map(|v| {
7999 if let Some(s) = v.as_str() {
8000 s.to_string()
8001 } else {
8002 serde_json::to_string(v).unwrap_or_default()
8003 }
8004 })
8005 .ok_or("PolicyDocument is required")?;
8006
8007 let policy = ResourcePolicy {
8008 policy_name: policy_name.clone(),
8009 policy_document,
8010 last_updated_time: Utc::now().timestamp_millis(),
8011 };
8012
8013 let mut logs_accounts = self.logs_state.write();
8014 let state = logs_accounts.get_or_create(&self.account_id);
8015 state.resource_policies.insert(policy_name.clone(), policy);
8016
8017 Ok(ProvisionResult::new(policy_name))
8018 }
8019
8020 fn delete_logs_resource_policy(&self, physical_id: &str) -> Result<(), String> {
8021 let mut logs_accounts = self.logs_state.write();
8022 let state = logs_accounts.get_or_create(&self.account_id);
8023 state.resource_policies.remove(physical_id);
8024 Ok(())
8025 }
8026
8027 fn create_logs_query_definition(
8028 &self,
8029 resource: &ResourceDefinition,
8030 ) -> Result<ProvisionResult, String> {
8031 let props = &resource.properties;
8032 let name = props
8033 .get("Name")
8034 .and_then(|v| v.as_str())
8035 .ok_or("Name is required")?
8036 .to_string();
8037 let query_string = props
8038 .get("QueryString")
8039 .and_then(|v| v.as_str())
8040 .ok_or("QueryString is required")?
8041 .to_string();
8042 let log_group_names: Vec<String> = props
8043 .get("LogGroupNames")
8044 .and_then(|v| v.as_array())
8045 .map(|arr| {
8046 arr.iter()
8047 .filter_map(|v| v.as_str().map(String::from))
8048 .collect()
8049 })
8050 .unwrap_or_default();
8051
8052 let id = Uuid::new_v4().to_string();
8053 let qd = QueryDefinition {
8054 query_definition_id: id.clone(),
8055 name,
8056 query_string,
8057 log_group_names,
8058 last_modified: Utc::now().timestamp_millis(),
8059 };
8060
8061 let mut logs_accounts = self.logs_state.write();
8062 let state = logs_accounts.get_or_create(&self.account_id);
8063 state.query_definitions.insert(id.clone(), qd);
8064
8065 Ok(ProvisionResult::new(id.clone()).with("QueryDefinitionId", id))
8066 }
8067
8068 fn delete_logs_query_definition(&self, physical_id: &str) -> Result<(), String> {
8069 let mut logs_accounts = self.logs_state.write();
8070 let state = logs_accounts.get_or_create(&self.account_id);
8071 state.query_definitions.remove(physical_id);
8072 Ok(())
8073 }
8074
8075 fn create_logs_delivery_destination(
8076 &self,
8077 resource: &ResourceDefinition,
8078 ) -> Result<ProvisionResult, String> {
8079 let props = &resource.properties;
8080 let name = props
8081 .get("Name")
8082 .and_then(|v| v.as_str())
8083 .ok_or("Name is required")?
8084 .to_string();
8085 let output_format = props
8086 .get("OutputFormat")
8087 .and_then(|v| v.as_str())
8088 .map(String::from);
8089 let mut configuration: BTreeMap<String, String> = BTreeMap::new();
8090 if let Some(arn) = props.get("DestinationResourceArn").and_then(|v| v.as_str()) {
8091 configuration.insert("destinationResourceArn".to_string(), arn.to_string());
8092 }
8093 if let Some(cfg) = props
8094 .get("DeliveryDestinationConfiguration")
8095 .and_then(|v| v.as_object())
8096 {
8097 for (k, v) in cfg {
8098 if let Some(s) = v.as_str() {
8099 configuration.insert(k.clone(), s.to_string());
8100 }
8101 }
8102 }
8103 let policy = props.get("DeliveryDestinationPolicy").map(|v| {
8104 if let Some(s) = v.as_str() {
8105 s.to_string()
8106 } else {
8107 serde_json::to_string(v).unwrap_or_default()
8108 }
8109 });
8110
8111 let arn = format!(
8112 "arn:aws:logs:{}:{}:delivery-destination:{name}",
8113 self.region, self.account_id
8114 );
8115 let dd = DeliveryDestination {
8116 name: name.clone(),
8117 arn: arn.clone(),
8118 output_format,
8119 delivery_destination_configuration: configuration,
8120 tags: BTreeMap::new(),
8121 delivery_destination_policy: policy,
8122 };
8123
8124 let mut logs_accounts = self.logs_state.write();
8125 let state = logs_accounts.get_or_create(&self.account_id);
8126 state.delivery_destinations.insert(name.clone(), dd);
8127
8128 Ok(ProvisionResult::new(name).with("Arn", arn))
8129 }
8130
8131 fn delete_logs_delivery_destination(&self, physical_id: &str) -> Result<(), String> {
8132 let mut logs_accounts = self.logs_state.write();
8133 let state = logs_accounts.get_or_create(&self.account_id);
8134 state.delivery_destinations.remove(physical_id);
8135 Ok(())
8136 }
8137
8138 fn create_logs_delivery_source(
8139 &self,
8140 resource: &ResourceDefinition,
8141 ) -> Result<ProvisionResult, String> {
8142 let props = &resource.properties;
8143 let name = props
8144 .get("Name")
8145 .and_then(|v| v.as_str())
8146 .ok_or("Name is required")?
8147 .to_string();
8148 let resource_arns: Vec<String> = props
8149 .get("ResourceArn")
8150 .and_then(|v| v.as_str())
8151 .map(|s| vec![s.to_string()])
8152 .or_else(|| {
8153 props
8154 .get("ResourceArns")
8155 .and_then(|v| v.as_array())
8156 .map(|arr| {
8157 arr.iter()
8158 .filter_map(|v| v.as_str().map(String::from))
8159 .collect()
8160 })
8161 })
8162 .unwrap_or_default();
8163 let log_type = props
8164 .get("LogType")
8165 .and_then(|v| v.as_str())
8166 .ok_or("LogType is required")?
8167 .to_string();
8168 let service = props
8169 .get("Service")
8170 .and_then(|v| v.as_str())
8171 .unwrap_or("")
8172 .to_string();
8173
8174 let arn = format!(
8175 "arn:aws:logs:{}:{}:delivery-source:{name}",
8176 self.region, self.account_id
8177 );
8178 let ds = DeliverySource {
8179 name: name.clone(),
8180 arn: arn.clone(),
8181 resource_arns,
8182 service,
8183 log_type,
8184 tags: BTreeMap::new(),
8185 created_at: chrono::Utc::now().timestamp_millis(),
8186 };
8187
8188 let mut logs_accounts = self.logs_state.write();
8189 let state = logs_accounts.get_or_create(&self.account_id);
8190 state.delivery_sources.insert(name.clone(), ds);
8191
8192 Ok(ProvisionResult::new(name).with("Arn", arn))
8193 }
8194
8195 fn delete_logs_delivery_source(&self, physical_id: &str) -> Result<(), String> {
8196 let mut logs_accounts = self.logs_state.write();
8197 let state = logs_accounts.get_or_create(&self.account_id);
8198 state.delivery_sources.remove(physical_id);
8199 Ok(())
8200 }
8201
8202 fn create_logs_delivery(
8203 &self,
8204 resource: &ResourceDefinition,
8205 ) -> Result<ProvisionResult, String> {
8206 let props = &resource.properties;
8207 let delivery_source_name = props
8208 .get("DeliverySourceName")
8209 .and_then(|v| v.as_str())
8210 .ok_or("DeliverySourceName is required")?
8211 .to_string();
8212 let delivery_destination_arn = props
8213 .get("DeliveryDestinationArn")
8214 .and_then(|v| v.as_str())
8215 .ok_or("DeliveryDestinationArn is required")?
8216 .to_string();
8217 let delivery_destination_type = if delivery_destination_arn.contains(":s3:") {
8219 "S3".to_string()
8220 } else if delivery_destination_arn.contains(":firehose:") {
8221 "FH".to_string()
8222 } else {
8223 "CWL".to_string()
8224 };
8225
8226 let id = Uuid::new_v4().simple().to_string();
8227 let arn = format!(
8228 "arn:aws:logs:{}:{}:delivery:{id}",
8229 self.region, self.account_id
8230 );
8231 let delivery = Delivery {
8232 id: id.clone(),
8233 delivery_source_name,
8234 delivery_destination_arn,
8235 delivery_destination_type,
8236 arn: arn.clone(),
8237 tags: BTreeMap::new(),
8238 field_delimiter: None,
8239 record_fields: Vec::new(),
8240 s3_delivery_configuration: None,
8241 created_at: chrono::Utc::now().timestamp_millis(),
8242 };
8243
8244 let mut logs_accounts = self.logs_state.write();
8245 let state = logs_accounts.get_or_create(&self.account_id);
8246 state.deliveries.insert(id.clone(), delivery);
8247
8248 Ok(ProvisionResult::new(id.clone())
8249 .with("DeliveryId", id)
8250 .with("Arn", arn))
8251 }
8252
8253 fn delete_logs_delivery(&self, physical_id: &str) -> Result<(), String> {
8254 let mut logs_accounts = self.logs_state.write();
8255 let state = logs_accounts.get_or_create(&self.account_id);
8256 state.deliveries.remove(physical_id);
8257 Ok(())
8258 }
8259
8260 fn invoke_lambda_sync(&self, function_arn: &str, payload: &str) -> Result<(), String> {
8264 let delivery = self.delivery.clone();
8265 let function_arn = function_arn.to_string();
8266 let payload = payload.to_string();
8267 std::thread::scope(|s| {
8268 s.spawn(|| {
8269 let rt = tokio::runtime::Builder::new_current_thread()
8270 .enable_all()
8271 .build()
8272 .map_err(|e| format!("Failed to create runtime: {e}"))?;
8273 rt.block_on(async {
8274 match delivery.invoke_lambda(&function_arn, &payload).await {
8275 Some(Ok(_)) => {
8276 tracing::info!(
8277 "Custom resource Lambda {} invoked successfully",
8278 function_arn
8279 );
8280 Ok(())
8281 }
8282 Some(Err(e)) => {
8283 tracing::warn!(
8284 "Custom resource Lambda {} invocation failed: {e}",
8285 function_arn
8286 );
8287 Err(format!("Lambda invocation failed: {e}"))
8288 }
8289 None => {
8290 tracing::warn!(
8291 "No Lambda delivery configured; skipping custom resource invocation for {}",
8292 function_arn
8293 );
8294 Ok(())
8295 }
8296 }
8297 })
8298 })
8299 .join()
8300 .map_err(|_| "Lambda invocation thread panicked".to_string())?
8301 })
8302 }
8303
8304 fn create_custom_resource(&self, resource: &ResourceDefinition) -> Result<String, String> {
8305 let props = &resource.properties;
8306 let service_token = props
8307 .get("ServiceToken")
8308 .and_then(|v| v.as_str())
8309 .ok_or("Custom resource requires ServiceToken property")?;
8310
8311 let request_id = Uuid::new_v4().to_string();
8312
8313 let event = serde_json::json!({
8315 "RequestType": "Create",
8316 "ServiceToken": service_token,
8317 "StackId": self.stack_id,
8318 "RequestId": request_id,
8319 "ResourceType": resource.resource_type,
8320 "LogicalResourceId": resource.logical_id,
8321 "ResourceProperties": props,
8322 });
8323
8324 let payload = serde_json::to_string(&event).map_err(|e| e.to_string())?;
8325 self.invoke_lambda_sync(service_token, &payload)?;
8326
8327 let physical_id = format!("{}-{}", resource.logical_id, &request_id[..8]);
8330 Ok(physical_id)
8331 }
8332
8333 fn delete_custom_resource(&self, resource: &StackResource) -> Result<(), String> {
8334 let service_token = match &resource.service_token {
8335 Some(token) => token.clone(),
8336 None => {
8337 return Ok(());
8339 }
8340 };
8341
8342 let request_id = Uuid::new_v4().to_string();
8343
8344 let event = serde_json::json!({
8345 "RequestType": "Delete",
8346 "ServiceToken": service_token,
8347 "StackId": self.stack_id,
8348 "RequestId": request_id,
8349 "ResourceType": resource.resource_type,
8350 "LogicalResourceId": resource.logical_id,
8351 "PhysicalResourceId": resource.physical_id,
8352 });
8353
8354 let payload = serde_json::to_string(&event).map_err(|e| e.to_string())?;
8355
8356 if let Err(e) = self.invoke_lambda_sync(&service_token, &payload) {
8358 tracing::warn!(
8359 "Custom resource delete Lambda invocation failed for {}: {e}",
8360 resource.logical_id
8361 );
8362 }
8363 Ok(())
8364 }
8365
8366 fn create_application_autoscaling_scalable_target(
8369 &self,
8370 resource: &ResourceDefinition,
8371 ) -> Result<ProvisionResult, String> {
8372 let props = &resource.properties;
8373 let service_namespace = props
8374 .get("ServiceNamespace")
8375 .and_then(|v| v.as_str())
8376 .ok_or_else(|| "ServiceNamespace is required".to_string())?
8377 .to_string();
8378 let resource_id = props
8379 .get("ResourceId")
8380 .and_then(|v| v.as_str())
8381 .ok_or_else(|| "ResourceId is required".to_string())?
8382 .to_string();
8383 let scalable_dimension = props
8384 .get("ScalableDimension")
8385 .and_then(|v| v.as_str())
8386 .ok_or_else(|| "ScalableDimension is required".to_string())?
8387 .to_string();
8388 let min_capacity = props
8389 .get("MinCapacity")
8390 .and_then(|v| v.as_i64())
8391 .map(|n| n as i32)
8392 .ok_or_else(|| "MinCapacity is required".to_string())?;
8393 let max_capacity = props
8394 .get("MaxCapacity")
8395 .and_then(|v| v.as_i64())
8396 .map(|n| n as i32)
8397 .ok_or_else(|| "MaxCapacity is required".to_string())?;
8398 if min_capacity > max_capacity {
8399 return Err("MinCapacity must be <= MaxCapacity".to_string());
8400 }
8401 let role_arn = props
8402 .get("RoleARN")
8403 .and_then(|v| v.as_str())
8404 .map(|s| s.to_string());
8405 let suspended_state = props.get("SuspendedState").map(|v| AppasSuspendedState {
8406 dynamic_scaling_in_suspended: v
8407 .get("DynamicScalingInSuspended")
8408 .and_then(|x| x.as_bool()),
8409 dynamic_scaling_out_suspended: v
8410 .get("DynamicScalingOutSuspended")
8411 .and_then(|x| x.as_bool()),
8412 scheduled_scaling_suspended: v
8413 .get("ScheduledScalingSuspended")
8414 .and_then(|x| x.as_bool()),
8415 });
8416
8417 let arn = format!(
8418 "arn:aws:application-autoscaling:{}:{}:scalable-target/{}",
8419 self.region,
8420 self.account_id,
8421 &Uuid::new_v4().simple().to_string()[..10]
8422 );
8423 let role = role_arn.unwrap_or_else(|| {
8424 let suffix = match service_namespace.as_str() {
8425 "ecs" => "ECSService",
8426 "elasticmapreduce" => "EMRContainerService",
8427 "ec2" => "EC2SpotFleetRequest",
8428 "appstream" => "ApplicationAutoScaling_AppStreamFleet",
8429 "dynamodb" => "DynamoDBTable",
8430 "rds" => "RDSCluster",
8431 "sagemaker" => "SageMakerEndpoint",
8432 "lambda" => "LambdaConcurrency",
8433 "elasticache" => "ElastiCacheRG",
8434 "cassandra" => "CassandraTable",
8435 "kafka" => "KafkaCluster",
8436 _ => "ApplicationAutoScaling_Default",
8437 };
8438 format!(
8439 "arn:aws:iam::{}:role/aws-service-role/applicationautoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_{}",
8440 self.account_id, suffix
8441 )
8442 });
8443
8444 let mut state = self.app_autoscaling_state.write();
8445 let account = state.accounts.entry(self.account_id.clone()).or_default();
8446 let key = (
8447 service_namespace.clone(),
8448 resource_id.clone(),
8449 scalable_dimension.clone(),
8450 );
8451 let target = AppasScalableTarget {
8452 arn: arn.clone(),
8453 service_namespace: service_namespace.clone(),
8454 resource_id: resource_id.clone(),
8455 scalable_dimension: scalable_dimension.clone(),
8456 min_capacity,
8457 max_capacity,
8458 role_arn: role,
8459 creation_time: Utc::now(),
8460 suspended_state,
8461 predicted_capacity: None,
8462 };
8463 account.scalable_targets.insert(key, target);
8464
8465 Ok(ProvisionResult::new(resource_id.clone())
8466 .with("ScalableTargetARN", arn)
8467 .with("ServiceNamespace", service_namespace)
8468 .with("ScalableDimension", scalable_dimension))
8469 }
8470
8471 fn create_application_autoscaling_scaling_policy(
8472 &self,
8473 resource: &ResourceDefinition,
8474 ) -> Result<ProvisionResult, String> {
8475 let props = &resource.properties;
8476 let policy_name = props
8477 .get("PolicyName")
8478 .and_then(|v| v.as_str())
8479 .ok_or_else(|| "PolicyName is required".to_string())?
8480 .to_string();
8481 let service_namespace = props
8482 .get("ServiceNamespace")
8483 .and_then(|v| v.as_str())
8484 .ok_or_else(|| "ServiceNamespace is required".to_string())?
8485 .to_string();
8486 let resource_id = props
8487 .get("ResourceId")
8488 .and_then(|v| v.as_str())
8489 .ok_or_else(|| "ResourceId is required".to_string())?
8490 .to_string();
8491 let scalable_dimension = props
8492 .get("ScalableDimension")
8493 .and_then(|v| v.as_str())
8494 .ok_or_else(|| "ScalableDimension is required".to_string())?
8495 .to_string();
8496 let policy_type = props
8497 .get("PolicyType")
8498 .and_then(|v| v.as_str())
8499 .unwrap_or("StepScaling")
8500 .to_string();
8501 let step_cfg = props.get("StepScalingPolicyConfiguration").cloned();
8502 let tt_cfg = props
8503 .get("TargetTrackingScalingPolicyConfiguration")
8504 .cloned();
8505 let pred_cfg = props.get("PredictiveScalingPolicyConfiguration").cloned();
8506
8507 let target_key = (
8508 service_namespace.clone(),
8509 resource_id.clone(),
8510 scalable_dimension.clone(),
8511 );
8512 let policy_key = (
8513 service_namespace.clone(),
8514 resource_id.clone(),
8515 scalable_dimension.clone(),
8516 policy_name.clone(),
8517 );
8518
8519 let mut state = self.app_autoscaling_state.write();
8520 let account = state.accounts.entry(self.account_id.clone()).or_default();
8521 if !account.scalable_targets.contains_key(&target_key) {
8522 return Err(format!(
8523 "No scalable target registered for ServiceNamespace={} ResourceId={} ScalableDimension={}",
8524 service_namespace, resource_id, scalable_dimension
8525 ));
8526 }
8527 let arn = format!(
8528 "arn:aws:autoscaling:{}:{}:scalingPolicy:{}:resource/{}/{}:policyName/{}",
8529 self.region,
8530 self.account_id,
8531 Uuid::new_v4(),
8532 service_namespace,
8533 resource_id,
8534 policy_name
8535 );
8536 let policy = AppasScalingPolicy {
8537 arn: arn.clone(),
8538 policy_name: policy_name.clone(),
8539 service_namespace: service_namespace.clone(),
8540 resource_id: resource_id.clone(),
8541 scalable_dimension: scalable_dimension.clone(),
8542 policy_type: policy_type.clone(),
8543 creation_time: Utc::now(),
8544 step_scaling_policy_configuration: step_cfg,
8545 target_tracking_scaling_policy_configuration: tt_cfg,
8546 predictive_scaling_policy_configuration: pred_cfg,
8547 alarms: Vec::new(),
8548 last_applied_at: None,
8549 };
8550 account.scaling_policies.insert(policy_key, policy);
8551
8552 Ok(ProvisionResult::new(arn.clone())
8553 .with("PolicyName", policy_name)
8554 .with("ServiceNamespace", service_namespace)
8555 .with("ResourceId", resource_id)
8556 .with("ScalableDimension", scalable_dimension))
8557 }
8558
8559 fn delete_application_autoscaling_scalable_target(
8560 &self,
8561 physical_id: &str,
8562 attributes: &BTreeMap<String, String>,
8563 ) -> Result<(), String> {
8564 let namespace = attributes
8565 .get("ServiceNamespace")
8566 .cloned()
8567 .ok_or_else(|| "ServiceNamespace missing in attributes".to_string())?;
8568 let resource_id = physical_id.to_string();
8569 let dimension = attributes
8570 .get("ScalableDimension")
8571 .cloned()
8572 .ok_or_else(|| "ScalableDimension missing in attributes".to_string())?;
8573 let key = (namespace, resource_id.clone(), dimension);
8574
8575 let mut state = self.app_autoscaling_state.write();
8576 let account = state.accounts.entry(self.account_id.clone()).or_default();
8577 account.scalable_targets.remove(&key);
8578 account
8579 .scaling_policies
8580 .retain(|k, _| !(k.0 == key.0 && k.1 == key.1 && k.2 == key.2));
8581 account
8582 .scheduled_actions
8583 .retain(|k, _| !(k.0 == key.0 && k.1 == key.1 && k.2 == key.2));
8584 Ok(())
8585 }
8586
8587 fn delete_application_autoscaling_scaling_policy(
8588 &self,
8589 _physical_id: &str,
8590 attributes: &BTreeMap<String, String>,
8591 ) -> Result<(), String> {
8592 let policy_name = attributes
8593 .get("PolicyName")
8594 .cloned()
8595 .ok_or_else(|| "PolicyName missing in attributes".to_string())?;
8596 let namespace = attributes
8597 .get("ServiceNamespace")
8598 .cloned()
8599 .ok_or_else(|| "ServiceNamespace missing in attributes".to_string())?;
8600 let resource_id = attributes
8601 .get("ResourceId")
8602 .cloned()
8603 .ok_or_else(|| "ResourceId missing in attributes".to_string())?;
8604 let dimension = attributes
8605 .get("ScalableDimension")
8606 .cloned()
8607 .ok_or_else(|| "ScalableDimension missing in attributes".to_string())?;
8608 let key = (namespace, resource_id, dimension, policy_name);
8609
8610 let mut state = self.app_autoscaling_state.write();
8611 let account = state.accounts.entry(self.account_id.clone()).or_default();
8612 account.scaling_policies.remove(&key);
8613 Ok(())
8614 }
8615
8616 fn create_firehose_delivery_stream(
8619 &self,
8620 resource: &ResourceDefinition,
8621 ) -> Result<ProvisionResult, String> {
8622 let props = &resource.properties;
8623 let name = props
8624 .get("DeliveryStreamName")
8625 .and_then(|v| v.as_str())
8626 .unwrap_or(&resource.logical_id)
8627 .to_string();
8628
8629 let arn = format!(
8630 "arn:aws:firehose:{}:{}:deliverystream/{}",
8631 self.region, self.account_id, name
8632 );
8633 let stream_type = props
8634 .get("DeliveryStreamType")
8635 .and_then(|v| v.as_str())
8636 .unwrap_or("DirectPut")
8637 .to_string();
8638
8639 let has_s3 = props.get("S3DestinationConfiguration").is_some();
8640 let has_extended_s3 = props.get("ExtendedS3DestinationConfiguration").is_some();
8641 if has_s3 && has_extended_s3 {
8642 return Err("Only one of S3DestinationConfiguration or ExtendedS3DestinationConfiguration may be set".to_string());
8643 }
8644 let destination = Some(if let Some(s3) = props.get("S3DestinationConfiguration") {
8645 parse_firehose_s3_destination(s3)?
8646 } else if let Some(s3) = props.get("ExtendedS3DestinationConfiguration") {
8647 parse_firehose_s3_destination(s3)?
8648 } else {
8649 return Err("Delivery stream requires a destination configuration".to_string());
8650 });
8651
8652 let mut tags = BTreeMap::new();
8653 if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
8654 for tag in arr {
8655 if let (Some(k), Some(v)) = (
8656 tag.get("Key").and_then(|v| v.as_str()),
8657 tag.get("Value").and_then(|v| v.as_str()),
8658 ) {
8659 tags.insert(k.to_string(), v.to_string());
8660 }
8661 }
8662 }
8663
8664 let stream = DeliveryStream {
8665 name: name.clone(),
8666 arn: arn.clone(),
8667 status: "ACTIVE".to_string(),
8668 stream_type: stream_type.clone(),
8669 created_at: Utc::now(),
8670 last_update: Utc::now(),
8671 version_id: "1".to_string(),
8672 destination,
8673 tags,
8674 };
8675
8676 let mut state = self.firehose_state.write();
8677 let account = state.get_or_create(&self.account_id, &self.region);
8678 account
8679 .streams_mut(&self.region)
8680 .insert(name.clone(), stream);
8681
8682 let mut attributes = BTreeMap::new();
8683 attributes.insert("Arn".to_string(), arn.clone());
8684 attributes.insert("DeliveryStreamName".to_string(), name.clone());
8685
8686 Ok(ProvisionResult {
8687 physical_id: name,
8688 attributes,
8689 })
8690 }
8691
8692 fn delete_firehose_delivery_stream(&self, physical_id: &str) -> Result<(), String> {
8693 let mut state = self.firehose_state.write();
8694 let account = state.get_or_create(&self.account_id, &self.region);
8695 account.streams_mut(&self.region).remove(physical_id);
8696 Ok(())
8697 }
8698
8699 fn create_cognito_user_pool(
8702 &self,
8703 resource: &ResourceDefinition,
8704 ) -> Result<ProvisionResult, String> {
8705 let props = &resource.properties;
8706 let pool_name = props
8707 .get("PoolName")
8708 .and_then(|v| v.as_str())
8709 .unwrap_or(&resource.logical_id)
8710 .to_string();
8711
8712 let pool_id = format!(
8713 "{}_{}",
8714 self.region,
8715 Uuid::new_v4()
8716 .simple()
8717 .to_string()
8718 .chars()
8719 .take(9)
8720 .collect::<String>()
8721 );
8722 let arn = format!(
8723 "arn:aws:cognito-idp:{}:{}:userpool/{}",
8724 self.region, self.account_id, pool_id
8725 );
8726 let now = Utc::now();
8727
8728 let password_policy = parse_cognito_password_policy(props.get("Policies"));
8729 let auto_verified = parse_cognito_string_array(props.get("AutoVerifiedAttributes"));
8730 let username_attributes = props
8731 .get("UsernameAttributes")
8732 .and_then(|v| v.as_array())
8733 .map(|_| parse_cognito_string_array(props.get("UsernameAttributes")));
8734 let alias_attributes = props
8735 .get("AliasAttributes")
8736 .and_then(|v| v.as_array())
8737 .map(|_| parse_cognito_string_array(props.get("AliasAttributes")));
8738 let mut schema_attributes = default_schema_attributes();
8739 if let Some(arr) = props.get("Schema").and_then(|v| v.as_array()) {
8740 for attr in arr {
8741 if let Some(parsed) = parse_cognito_schema_attribute(attr) {
8742 if !schema_attributes.iter().any(|a| a.name == parsed.name) {
8743 schema_attributes.push(parsed);
8744 }
8745 }
8746 }
8747 }
8748 let mfa_configuration = props
8749 .get("MfaConfiguration")
8750 .and_then(|v| v.as_str())
8751 .unwrap_or("OFF")
8752 .to_string();
8753 let user_pool_tier = props
8754 .get("UserPoolTier")
8755 .and_then(|v| v.as_str())
8756 .unwrap_or("ESSENTIALS")
8757 .to_string();
8758 let deletion_protection = props
8759 .get("DeletionProtection")
8760 .and_then(|v| v.as_str())
8761 .map(|s| s.to_string());
8762 let user_pool_tags = parse_cognito_tags(props.get("UserPoolTags"));
8763 let email_configuration =
8764 parse_cognito_email_configuration(props.get("EmailConfiguration"));
8765 let sms_configuration = parse_cognito_sms_configuration(props.get("SmsConfiguration"));
8766 let admin_create_user_config =
8767 parse_cognito_admin_create_user_config(props.get("AdminCreateUserConfig"));
8768 let account_recovery_setting =
8769 parse_cognito_account_recovery(props.get("AccountRecoverySetting"));
8770
8771 let signing = fakecloud_cognito::jwt::generate_pool_signing_key();
8775 let signing_key_pem = signing.private_key_pem;
8776 let signing_kid = signing.kid;
8777 let pool = UserPool {
8778 id: pool_id.clone(),
8779 name: pool_name,
8780 arn: arn.clone(),
8781 status: "ACTIVE".to_string(),
8782 creation_date: now,
8783 last_modified_date: now,
8784 policies: PoolPolicies {
8785 password_policy,
8786 sign_in_policy: SignInPolicy {
8787 allowed_first_auth_factors: vec!["PASSWORD".to_string()],
8788 },
8789 },
8790 auto_verified_attributes: auto_verified,
8791 username_attributes,
8792 alias_attributes,
8793 schema_attributes,
8794 lambda_config: None,
8795 mfa_configuration,
8796 email_configuration,
8797 sms_configuration,
8798 admin_create_user_config,
8799 user_pool_tags,
8800 account_recovery_setting,
8801 deletion_protection,
8802 estimated_number_of_users: 0,
8803 software_token_mfa_configuration: None,
8804 sms_mfa_configuration: None,
8805 user_pool_tier,
8806 verification_message_template: None,
8807 signing_key_pem: Some(signing_key_pem),
8808 signing_kid: Some(signing_kid),
8809 email_verification_message: None,
8810 email_verification_subject: None,
8811 sms_verification_message: None,
8812 sms_authentication_message: None,
8813 device_configuration: None,
8814 user_attribute_update_settings: None,
8815 user_pool_add_ons: None,
8816 username_configuration: None,
8817 };
8818
8819 let mut accounts = self.cognito_state.write();
8820 let state = accounts.get_or_create(&self.account_id);
8821 state.user_pools.insert(pool_id.clone(), pool);
8822
8823 let provider_name = format!("cognito-idp.{}.amazonaws.com/{}", self.region, pool_id);
8824 let provider_url = format!("https://{provider_name}");
8825
8826 Ok(ProvisionResult::new(pool_id.clone())
8827 .with("Arn", arn)
8828 .with("ProviderName", provider_name)
8829 .with("ProviderURL", provider_url)
8830 .with("UserPoolId", pool_id))
8831 }
8832
8833 fn delete_cognito_user_pool(&self, physical_id: &str) -> Result<(), String> {
8834 let mut accounts = self.cognito_state.write();
8835 let state = accounts.get_or_create(&self.account_id);
8836 state.user_pools.remove(physical_id);
8837 state
8839 .user_pool_clients
8840 .retain(|_, c| c.user_pool_id != physical_id);
8841 state.users.remove(physical_id);
8842 state.groups.remove(physical_id);
8843 state.user_groups.remove(physical_id);
8844 state.identity_providers.remove(physical_id);
8845 state.resource_servers.remove(physical_id);
8846 state.import_jobs.remove(physical_id);
8847 state.domains.retain(|_, d| d.user_pool_id != physical_id);
8848 Ok(())
8849 }
8850
8851 fn create_cognito_user_pool_client(
8852 &self,
8853 resource: &ResourceDefinition,
8854 ) -> Result<ProvisionResult, String> {
8855 let props = &resource.properties;
8856 let pool_id = props
8857 .get("UserPoolId")
8858 .and_then(|v| v.as_str())
8859 .ok_or_else(|| "UserPoolId is required".to_string())?
8860 .to_string();
8861 let client_name = props
8862 .get("ClientName")
8863 .and_then(|v| v.as_str())
8864 .unwrap_or(&resource.logical_id)
8865 .to_string();
8866
8867 let mut accounts = self.cognito_state.write();
8868 let state = accounts.get_or_create(&self.account_id);
8869 if !state.user_pools.contains_key(&pool_id) {
8870 return Err(format!(
8872 "User pool {pool_id} does not exist yet — retry once it has been provisioned"
8873 ));
8874 }
8875
8876 let client_id: String = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple())
8877 .chars()
8878 .filter(|c| c.is_ascii_alphanumeric())
8879 .take(26)
8880 .collect::<String>()
8881 .to_lowercase();
8882 let generate_secret = props
8883 .get("GenerateSecret")
8884 .and_then(|v| v.as_bool())
8885 .unwrap_or(false);
8886 let client_secret = if generate_secret {
8887 use base64::Engine;
8888 let mut bytes = Vec::with_capacity(48);
8889 for _ in 0..3 {
8890 bytes.extend_from_slice(Uuid::new_v4().as_bytes());
8891 }
8892 Some(
8893 base64::engine::general_purpose::STANDARD
8894 .encode(&bytes)
8895 .chars()
8896 .take(51)
8897 .collect(),
8898 )
8899 } else {
8900 None
8901 };
8902
8903 let now = Utc::now();
8904 let client = UserPoolClient {
8905 client_id: client_id.clone(),
8906 client_name,
8907 user_pool_id: pool_id.clone(),
8908 client_secret: client_secret.clone(),
8909 explicit_auth_flows: parse_cognito_string_array(props.get("ExplicitAuthFlows")),
8910 token_validity_units: None,
8911 access_token_validity: props.get("AccessTokenValidity").and_then(|v| v.as_i64()),
8912 id_token_validity: props.get("IdTokenValidity").and_then(|v| v.as_i64()),
8913 refresh_token_validity: props.get("RefreshTokenValidity").and_then(|v| v.as_i64()),
8914 callback_urls: parse_cognito_string_array(props.get("CallbackURLs")),
8915 logout_urls: parse_cognito_string_array(props.get("LogoutURLs")),
8916 supported_identity_providers: parse_cognito_string_array(
8917 props.get("SupportedIdentityProviders"),
8918 ),
8919 allowed_o_auth_flows: parse_cognito_string_array(props.get("AllowedOAuthFlows")),
8920 allowed_o_auth_scopes: parse_cognito_string_array(props.get("AllowedOAuthScopes")),
8921 allowed_o_auth_flows_user_pool_client: props
8922 .get("AllowedOAuthFlowsUserPoolClient")
8923 .and_then(|v| v.as_bool())
8924 .unwrap_or(false),
8925 prevent_user_existence_errors: props
8926 .get("PreventUserExistenceErrors")
8927 .and_then(|v| v.as_str())
8928 .map(|s| s.to_string()),
8929 read_attributes: parse_cognito_string_array(props.get("ReadAttributes")),
8930 write_attributes: parse_cognito_string_array(props.get("WriteAttributes")),
8931 creation_date: now,
8932 last_modified_date: now,
8933 enable_token_revocation: props
8934 .get("EnableTokenRevocation")
8935 .and_then(|v| v.as_bool())
8936 .unwrap_or(true),
8937 auth_session_validity: props.get("AuthSessionValidity").and_then(|v| v.as_i64()),
8938 client_secrets: Vec::new(),
8939 refresh_token_rotation: None,
8940 };
8941
8942 state.user_pool_clients.insert(client_id.clone(), client);
8943
8944 let mut result = ProvisionResult::new(client_id.clone())
8945 .with("ClientId", client_id.clone())
8946 .with("Name", client_id);
8947 if let Some(secret) = client_secret {
8948 result = result.with("ClientSecret", secret);
8949 }
8950 Ok(result)
8951 }
8952
8953 fn delete_cognito_user_pool_client(&self, physical_id: &str) -> Result<(), String> {
8954 let mut accounts = self.cognito_state.write();
8955 let state = accounts.get_or_create(&self.account_id);
8956 state.user_pool_clients.remove(physical_id);
8957 Ok(())
8958 }
8959
8960 fn create_cognito_user_pool_domain(
8961 &self,
8962 resource: &ResourceDefinition,
8963 ) -> Result<ProvisionResult, String> {
8964 let props = &resource.properties;
8965 let domain = props
8966 .get("Domain")
8967 .and_then(|v| v.as_str())
8968 .ok_or_else(|| "Domain is required".to_string())?
8969 .to_string();
8970 let pool_id = props
8971 .get("UserPoolId")
8972 .and_then(|v| v.as_str())
8973 .ok_or_else(|| "UserPoolId is required".to_string())?
8974 .to_string();
8975 let custom_domain_config = props
8976 .get("CustomDomainConfig")
8977 .and_then(|v| v.as_object())
8978 .and_then(|m| {
8979 m.get("CertificateArn")
8980 .and_then(|v| v.as_str())
8981 .map(|s| CustomDomainConfig {
8982 certificate_arn: s.to_string(),
8983 })
8984 });
8985
8986 let mut accounts = self.cognito_state.write();
8987 let state = accounts.get_or_create(&self.account_id);
8988 if !state.user_pools.contains_key(&pool_id) {
8989 return Err(format!(
8990 "User pool {pool_id} does not exist yet — retry once it has been provisioned"
8991 ));
8992 }
8993 if state.domains.contains_key(&domain) {
8994 return Err(format!("Domain {domain} already exists"));
8995 }
8996 state.domains.insert(
8997 domain.clone(),
8998 UserPoolDomain {
8999 user_pool_id: pool_id,
9000 domain: domain.clone(),
9001 status: "ACTIVE".to_string(),
9002 custom_domain_config: custom_domain_config.clone(),
9003 creation_date: Utc::now(),
9004 },
9005 );
9006
9007 let cloudfront_distribution = if custom_domain_config.is_some() {
9008 format!("{domain}.cloudfront.net")
9009 } else {
9010 format!("{domain}.auth.{}.amazoncognito.com", self.region)
9011 };
9012
9013 Ok(ProvisionResult::new(domain.clone())
9014 .with("Domain", domain)
9015 .with("CloudFrontDistribution", cloudfront_distribution))
9016 }
9017
9018 fn delete_cognito_user_pool_domain(&self, physical_id: &str) -> Result<(), String> {
9019 let mut accounts = self.cognito_state.write();
9020 let state = accounts.get_or_create(&self.account_id);
9021 state.domains.remove(physical_id);
9022 Ok(())
9023 }
9024
9025 fn create_cognito_identity_pool(
9026 &self,
9027 resource: &ResourceDefinition,
9028 ) -> Result<ProvisionResult, String> {
9029 let props = &resource.properties;
9030 let identity_pool_name = props
9031 .get("IdentityPoolName")
9032 .and_then(|v| v.as_str())
9033 .unwrap_or(&resource.logical_id)
9034 .to_string();
9035 let allow_unauth = props
9036 .get("AllowUnauthenticatedIdentities")
9037 .and_then(|v| v.as_bool())
9038 .unwrap_or(false);
9039 let allow_classic = props
9040 .get("AllowClassicFlow")
9041 .and_then(|v| v.as_bool())
9042 .unwrap_or(false);
9043 let developer_provider_name = props
9044 .get("DeveloperProviderName")
9045 .and_then(|v| v.as_str())
9046 .map(|s| s.to_string());
9047 let cognito_identity_providers = props
9048 .get("CognitoIdentityProviders")
9049 .and_then(|v| v.as_array())
9050 .map(|arr| {
9051 arr.iter()
9052 .filter_map(|p| {
9053 let obj = p.as_object()?;
9054 let provider_name = obj
9055 .get("ProviderName")
9056 .and_then(|v| v.as_str())?
9057 .to_string();
9058 let client_id = obj.get("ClientId").and_then(|v| v.as_str())?.to_string();
9059 let server_side_token_check = obj
9060 .get("ServerSideTokenCheck")
9061 .and_then(|v| v.as_bool())
9062 .unwrap_or(false);
9063 Some(CognitoIdentityProvider {
9064 provider_name,
9065 client_id,
9066 server_side_token_check,
9067 })
9068 })
9069 .collect::<Vec<_>>()
9070 })
9071 .unwrap_or_default();
9072 let open_id_connect_provider_arns =
9073 parse_cognito_string_array(props.get("OpenIdConnectProviderARNs"));
9074 let saml_provider_arns = parse_cognito_string_array(props.get("SamlProviderARNs"));
9075 let supported_login_providers = props
9076 .get("SupportedLoginProviders")
9077 .and_then(|v| v.as_object())
9078 .map(|m| {
9079 m.iter()
9080 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
9081 .collect()
9082 })
9083 .unwrap_or_default();
9084 let identity_pool_tags = parse_cognito_tags(props.get("IdentityPoolTags"));
9085 let cognito_streams = props.get("CognitoStreams").cloned();
9086 let push_sync = props.get("PushSync").cloned();
9087
9088 let identity_pool_id = format!("{}:{}", self.region, Uuid::new_v4());
9091
9092 let pool = IdentityPool {
9093 identity_pool_id: identity_pool_id.clone(),
9094 identity_pool_name,
9095 allow_unauthenticated_identities: allow_unauth,
9096 allow_classic_flow: allow_classic,
9097 developer_provider_name,
9098 cognito_identity_providers,
9099 open_id_connect_provider_arns,
9100 saml_provider_arns,
9101 supported_login_providers,
9102 cognito_streams,
9103 push_sync,
9104 identity_pool_tags,
9105 creation_date: Utc::now(),
9106 };
9107
9108 let mut accounts = self.cognito_state.write();
9109 let state = accounts.get_or_create(&self.account_id);
9110 state.identity_pools.insert(identity_pool_id.clone(), pool);
9111
9112 Ok(ProvisionResult::new(identity_pool_id.clone()).with("Name", identity_pool_id))
9113 }
9114
9115 fn delete_cognito_identity_pool(&self, physical_id: &str) -> Result<(), String> {
9116 let mut accounts = self.cognito_state.write();
9117 let state = accounts.get_or_create(&self.account_id);
9118 state.identity_pools.remove(physical_id);
9119 state
9121 .identity_pool_role_attachments
9122 .retain(|_, a| a.identity_pool_id != physical_id);
9123 Ok(())
9124 }
9125
9126 fn create_cognito_identity_pool_role_attachment(
9127 &self,
9128 resource: &ResourceDefinition,
9129 ) -> Result<ProvisionResult, String> {
9130 let props = &resource.properties;
9131 let identity_pool_id = props
9132 .get("IdentityPoolId")
9133 .and_then(|v| v.as_str())
9134 .ok_or_else(|| "IdentityPoolId is required".to_string())?
9135 .to_string();
9136
9137 let mut accounts = self.cognito_state.write();
9138 let state = accounts.get_or_create(&self.account_id);
9139 if !state.identity_pools.contains_key(&identity_pool_id) {
9140 return Err(format!(
9141 "Identity pool {identity_pool_id} does not exist yet — retry once it has been provisioned"
9142 ));
9143 }
9144
9145 let roles = props
9146 .get("Roles")
9147 .and_then(|v| v.as_object())
9148 .map(|m| {
9149 m.iter()
9150 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
9151 .collect()
9152 })
9153 .unwrap_or_default();
9154 let role_mappings = props
9155 .get("RoleMappings")
9156 .and_then(|v| v.as_object())
9157 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
9158 .unwrap_or_default();
9159
9160 let attachment_id = Uuid::new_v4().simple().to_string();
9161 let physical_id = format!("{identity_pool_id}:{attachment_id}");
9162 let attachment = IdentityPoolRoleAttachment {
9163 identity_pool_id: identity_pool_id.clone(),
9164 attachment_id,
9165 roles,
9166 role_mappings,
9167 };
9168 state
9169 .identity_pool_role_attachments
9170 .insert(physical_id.clone(), attachment);
9171
9172 Ok(ProvisionResult::new(physical_id))
9173 }
9174
9175 fn delete_cognito_identity_pool_role_attachment(
9176 &self,
9177 physical_id: &str,
9178 ) -> Result<(), String> {
9179 let mut accounts = self.cognito_state.write();
9180 let state = accounts.get_or_create(&self.account_id);
9181 state.identity_pool_role_attachments.remove(physical_id);
9182 Ok(())
9183 }
9184
9185 fn create_rds_subnet_group(
9188 &self,
9189 resource: &ResourceDefinition,
9190 ) -> Result<ProvisionResult, String> {
9191 let props = &resource.properties;
9192 let name = props
9193 .get("DBSubnetGroupName")
9194 .and_then(|v| v.as_str())
9195 .unwrap_or(&resource.logical_id)
9196 .to_string();
9197 let description = props
9198 .get("DBSubnetGroupDescription")
9199 .and_then(|v| v.as_str())
9200 .unwrap_or("")
9201 .to_string();
9202 let subnet_ids: Vec<String> = props
9203 .get("SubnetIds")
9204 .and_then(|v| v.as_array())
9205 .map(|arr| {
9206 arr.iter()
9207 .filter_map(|v| v.as_str().map(|s| s.to_string()))
9208 .collect()
9209 })
9210 .unwrap_or_default();
9211 let tags = parse_rds_tags(props.get("Tags"));
9212 let mut accounts = self.rds_state.write();
9213 let state = accounts.get_or_create(&self.account_id);
9214 let arn = state.db_subnet_group_arn(&name);
9215 let group = DbSubnetGroup {
9216 db_subnet_group_name: name.clone(),
9217 db_subnet_group_arn: arn.clone(),
9218 db_subnet_group_description: description,
9219 vpc_id: String::new(),
9220 subnet_ids,
9221 subnet_availability_zones: Vec::new(),
9222 tags,
9223 };
9224 state.subnet_groups.insert(name.clone(), group);
9225 Ok(ProvisionResult::new(name).with("Arn", arn))
9226 }
9227
9228 fn delete_rds_subnet_group(&self, physical_id: &str) -> Result<(), String> {
9229 let mut accounts = self.rds_state.write();
9230 let state = accounts.get_or_create(&self.account_id);
9231 state.subnet_groups.remove(physical_id);
9232 Ok(())
9233 }
9234
9235 fn create_rds_parameter_group(
9236 &self,
9237 resource: &ResourceDefinition,
9238 ) -> Result<ProvisionResult, String> {
9239 let props = &resource.properties;
9240 let name = props
9241 .get("DBParameterGroupName")
9242 .and_then(|v| v.as_str())
9243 .unwrap_or(&resource.logical_id)
9244 .to_string();
9245 let family = props
9246 .get("Family")
9247 .and_then(|v| v.as_str())
9248 .unwrap_or("postgres16")
9249 .to_string();
9250 let description = props
9251 .get("Description")
9252 .and_then(|v| v.as_str())
9253 .unwrap_or("")
9254 .to_string();
9255 let parameters: std::collections::BTreeMap<String, String> = props
9256 .get("Parameters")
9257 .and_then(|v| v.as_object())
9258 .map(|m| {
9259 m.iter()
9260 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
9261 .collect()
9262 })
9263 .unwrap_or_default();
9264 let tags = parse_rds_tags(props.get("Tags"));
9265
9266 let mut accounts = self.rds_state.write();
9267 let state = accounts.get_or_create(&self.account_id);
9268 let arn = state.db_parameter_group_arn(&name);
9269 let group = DbParameterGroup {
9270 db_parameter_group_name: name.clone(),
9271 db_parameter_group_arn: arn.clone(),
9272 db_parameter_group_family: family,
9273 description,
9274 parameters,
9275 tags,
9276 };
9277 state.parameter_groups.insert(name.clone(), group);
9278 Ok(ProvisionResult::new(name).with("Arn", arn))
9279 }
9280
9281 fn delete_rds_parameter_group(&self, physical_id: &str) -> Result<(), String> {
9282 let mut accounts = self.rds_state.write();
9283 let state = accounts.get_or_create(&self.account_id);
9284 state.parameter_groups.remove(physical_id);
9285 Ok(())
9286 }
9287
9288 fn create_rds_cluster_parameter_group(
9289 &self,
9290 resource: &ResourceDefinition,
9291 ) -> Result<ProvisionResult, String> {
9292 let props = &resource.properties;
9293 let name = props
9294 .get("DBClusterParameterGroupName")
9295 .or_else(|| props.get("Name"))
9296 .and_then(|v| v.as_str())
9297 .unwrap_or(&resource.logical_id)
9298 .to_string();
9299 let family = props
9300 .get("Family")
9301 .and_then(|v| v.as_str())
9302 .unwrap_or("aurora-postgresql15")
9303 .to_string();
9304 let description = props
9305 .get("Description")
9306 .and_then(|v| v.as_str())
9307 .unwrap_or("")
9308 .to_string();
9309 let arn = format!(
9310 "arn:aws:rds:{}:{}:cluster-pg:{}",
9311 self.region, self.account_id, name
9312 );
9313 let entry = serde_json::json!({
9314 "DBClusterParameterGroupName": name,
9315 "DBClusterParameterGroupArn": arn,
9316 "DBParameterGroupFamily": family,
9317 "Description": description,
9318 });
9319 let mut accounts = self.rds_state.write();
9320 let state = accounts.get_or_create(&self.account_id);
9321 rds_extras_mut(state, "cluster_param_groups").insert(name.clone(), entry);
9322 Ok(ProvisionResult::new(name).with("Arn", arn))
9323 }
9324
9325 fn delete_rds_cluster_parameter_group(&self, physical_id: &str) -> Result<(), String> {
9326 let mut accounts = self.rds_state.write();
9327 let state = accounts.get_or_create(&self.account_id);
9328 if let Some(m) = state.extras.get_mut("cluster_param_groups") {
9329 m.remove(physical_id);
9330 }
9331 Ok(())
9332 }
9333
9334 fn create_rds_option_group(
9335 &self,
9336 resource: &ResourceDefinition,
9337 ) -> Result<ProvisionResult, String> {
9338 let props = &resource.properties;
9339 let name = props
9340 .get("OptionGroupName")
9341 .and_then(|v| v.as_str())
9342 .unwrap_or(&resource.logical_id)
9343 .to_string();
9344 let engine_name = props
9345 .get("EngineName")
9346 .and_then(|v| v.as_str())
9347 .unwrap_or("mysql")
9348 .to_string();
9349 let major_engine_version = props
9350 .get("MajorEngineVersion")
9351 .and_then(|v| v.as_str())
9352 .unwrap_or("8.0")
9353 .to_string();
9354 let description = props
9355 .get("OptionGroupDescription")
9356 .and_then(|v| v.as_str())
9357 .unwrap_or("")
9358 .to_string();
9359 let arn = format!(
9360 "arn:aws:rds:{}:{}:og:{}",
9361 self.region, self.account_id, name
9362 );
9363 let entry = serde_json::json!({
9364 "OptionGroupName": name,
9365 "OptionGroupArn": arn,
9366 "EngineName": engine_name,
9367 "MajorEngineVersion": major_engine_version,
9368 "OptionGroupDescription": description,
9369 });
9370 let mut accounts = self.rds_state.write();
9371 let state = accounts.get_or_create(&self.account_id);
9372 rds_extras_mut(state, "option_groups").insert(name.clone(), entry);
9373 Ok(ProvisionResult::new(name).with("Arn", arn))
9374 }
9375
9376 fn delete_rds_option_group(&self, physical_id: &str) -> Result<(), String> {
9377 let mut accounts = self.rds_state.write();
9378 let state = accounts.get_or_create(&self.account_id);
9379 if let Some(m) = state.extras.get_mut("option_groups") {
9380 m.remove(physical_id);
9381 }
9382 Ok(())
9383 }
9384
9385 fn create_rds_event_subscription(
9386 &self,
9387 resource: &ResourceDefinition,
9388 ) -> Result<ProvisionResult, String> {
9389 let props = &resource.properties;
9390 let name = props
9391 .get("SubscriptionName")
9392 .and_then(|v| v.as_str())
9393 .unwrap_or(&resource.logical_id)
9394 .to_string();
9395 let sns_topic_arn = props
9396 .get("SnsTopicArn")
9397 .and_then(|v| v.as_str())
9398 .unwrap_or("")
9399 .to_string();
9400 let entry = serde_json::json!({
9401 "CustSubscriptionId": name,
9402 "SnsTopicArn": sns_topic_arn,
9403 "Status": "active",
9404 "Enabled": props.get("Enabled").and_then(|v| v.as_bool()).unwrap_or(true),
9405 });
9406 let mut accounts = self.rds_state.write();
9407 let state = accounts.get_or_create(&self.account_id);
9408 rds_extras_mut(state, "event_subscriptions").insert(name.clone(), entry);
9409 Ok(ProvisionResult::new(name))
9410 }
9411
9412 fn delete_rds_event_subscription(&self, physical_id: &str) -> Result<(), String> {
9413 let mut accounts = self.rds_state.write();
9414 let state = accounts.get_or_create(&self.account_id);
9415 if let Some(m) = state.extras.get_mut("event_subscriptions") {
9416 m.remove(physical_id);
9417 }
9418 Ok(())
9419 }
9420
9421 fn create_rds_security_group(
9422 &self,
9423 resource: &ResourceDefinition,
9424 ) -> Result<ProvisionResult, String> {
9425 let props = &resource.properties;
9426 let name = props
9427 .get("DBSecurityGroupName")
9428 .and_then(|v| v.as_str())
9429 .unwrap_or(&resource.logical_id)
9430 .to_string();
9431 let description = props
9432 .get("GroupDescription")
9433 .and_then(|v| v.as_str())
9434 .unwrap_or("")
9435 .to_string();
9436 let entry = serde_json::json!({
9437 "DBSecurityGroupName": name,
9438 "DBSecurityGroupDescription": description,
9439 "OwnerId": self.account_id,
9440 });
9441 let mut accounts = self.rds_state.write();
9442 let state = accounts.get_or_create(&self.account_id);
9443 rds_extras_mut(state, "security_groups").insert(name.clone(), entry);
9444 Ok(ProvisionResult::new(name))
9445 }
9446
9447 fn delete_rds_security_group(&self, physical_id: &str) -> Result<(), String> {
9448 let mut accounts = self.rds_state.write();
9449 let state = accounts.get_or_create(&self.account_id);
9450 if let Some(m) = state.extras.get_mut("security_groups") {
9451 m.remove(physical_id);
9452 }
9453 Ok(())
9454 }
9455
9456 fn create_rds_db_proxy(
9457 &self,
9458 resource: &ResourceDefinition,
9459 ) -> Result<ProvisionResult, String> {
9460 let props = &resource.properties;
9461 let name = props
9462 .get("DBProxyName")
9463 .and_then(|v| v.as_str())
9464 .unwrap_or(&resource.logical_id)
9465 .to_string();
9466 let engine_family = props
9467 .get("EngineFamily")
9468 .and_then(|v| v.as_str())
9469 .unwrap_or("POSTGRESQL")
9470 .to_string();
9471 let arn = format!(
9472 "arn:aws:rds:{}:{}:db-proxy:{}",
9473 self.region, self.account_id, name
9474 );
9475 let endpoint = format!("{name}.proxy-default.{}.rds.amazonaws.com", self.region);
9476 let entry = serde_json::json!({
9477 "DBProxyName": name,
9478 "DBProxyArn": arn,
9479 "Status": "available",
9480 "EngineFamily": engine_family,
9481 "Endpoint": endpoint,
9482 });
9483 let mut accounts = self.rds_state.write();
9484 let state = accounts.get_or_create(&self.account_id);
9485 rds_extras_mut(state, "proxies").insert(name.clone(), entry);
9486 Ok(ProvisionResult::new(name)
9487 .with("DBProxyArn", arn)
9488 .with("Endpoint", endpoint))
9489 }
9490
9491 fn delete_rds_db_proxy(&self, physical_id: &str) -> Result<(), String> {
9492 let mut accounts = self.rds_state.write();
9493 let state = accounts.get_or_create(&self.account_id);
9494 if let Some(m) = state.extras.get_mut("proxies") {
9495 m.remove(physical_id);
9496 }
9497 Ok(())
9498 }
9499
9500 fn create_rds_db_instance(
9501 &self,
9502 resource: &ResourceDefinition,
9503 ) -> Result<ProvisionResult, String> {
9504 let props = &resource.properties;
9505 let identifier = props
9506 .get("DBInstanceIdentifier")
9507 .and_then(|v| v.as_str())
9508 .map(String::from)
9509 .unwrap_or_else(|| {
9510 format!(
9511 "cfn-{}-{}",
9512 resource.logical_id.to_lowercase(),
9513 Uuid::new_v4().simple().to_string()[..8].to_lowercase()
9514 )
9515 });
9516 let class = props
9517 .get("DBInstanceClass")
9518 .and_then(|v| v.as_str())
9519 .unwrap_or("db.t4g.micro")
9520 .to_string();
9521 let engine = props
9522 .get("Engine")
9523 .and_then(|v| v.as_str())
9524 .unwrap_or("postgres")
9525 .to_string();
9526 let engine_version = props
9527 .get("EngineVersion")
9528 .and_then(|v| v.as_str())
9529 .unwrap_or("16.0")
9530 .to_string();
9531 let master_username = props
9532 .get("MasterUsername")
9533 .and_then(|v| v.as_str())
9534 .unwrap_or("admin")
9535 .to_string();
9536 let master_user_password = props
9537 .get("MasterUserPassword")
9538 .and_then(|v| v.as_str())
9539 .unwrap_or("")
9540 .to_string();
9541 let db_name = props
9542 .get("DBName")
9543 .and_then(|v| v.as_str())
9544 .map(String::from);
9545 let port = props
9546 .get("Port")
9547 .and_then(|v| v.as_i64())
9548 .map(|n| n as i32)
9549 .unwrap_or(5432);
9550 let allocated_storage = props
9551 .get("AllocatedStorage")
9552 .and_then(|v| {
9553 v.as_i64()
9554 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
9555 })
9556 .map(|n| n as i32)
9557 .unwrap_or(20);
9558 let publicly_accessible = props
9559 .get("PubliclyAccessible")
9560 .and_then(|v| v.as_bool())
9561 .unwrap_or(false);
9562 let deletion_protection = props
9563 .get("DeletionProtection")
9564 .and_then(|v| v.as_bool())
9565 .unwrap_or(false);
9566 let backup_retention_period = props
9567 .get("BackupRetentionPeriod")
9568 .and_then(|v| v.as_i64())
9569 .map(|n| n as i32)
9570 .unwrap_or(0);
9571 let multi_az = props
9572 .get("MultiAZ")
9573 .and_then(|v| v.as_bool())
9574 .unwrap_or(false);
9575 let availability_zone = props
9576 .get("AvailabilityZone")
9577 .and_then(|v| v.as_str())
9578 .map(String::from);
9579 let storage_type = props
9580 .get("StorageType")
9581 .and_then(|v| v.as_str())
9582 .map(String::from);
9583 let storage_encrypted = props
9584 .get("StorageEncrypted")
9585 .and_then(|v| v.as_bool())
9586 .unwrap_or(false);
9587 let kms_key_id = props
9588 .get("KmsKeyId")
9589 .and_then(|v| v.as_str())
9590 .map(String::from);
9591 let iam_database_authentication_enabled = props
9592 .get("EnableIAMDatabaseAuthentication")
9593 .and_then(|v| v.as_bool())
9594 .unwrap_or(false);
9595 let db_parameter_group_name = props
9596 .get("DBParameterGroupName")
9597 .and_then(|v| v.as_str())
9598 .map(String::from);
9599 let option_group_name = props
9600 .get("OptionGroupName")
9601 .and_then(|v| v.as_str())
9602 .map(String::from);
9603 let vpc_security_group_ids: Vec<String> = props
9604 .get("VPCSecurityGroups")
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 enabled_cloudwatch_logs_exports: Vec<String> = props
9613 .get("EnableCloudwatchLogsExports")
9614 .and_then(|v| v.as_array())
9615 .map(|arr| {
9616 arr.iter()
9617 .filter_map(|v| v.as_str().map(String::from))
9618 .collect()
9619 })
9620 .unwrap_or_default();
9621 let tags = parse_rds_tags(props.get("Tags"));
9622
9623 let mut accounts = self.rds_state.write();
9624 let state = accounts.get_or_create(&self.account_id);
9625 let arn = state.db_instance_arn(&identifier);
9626 let endpoint_address = format!(
9627 "{identifier}.cluster-fakecloud.{}.rds.amazonaws.com",
9628 state.region
9629 );
9630 let dbi_resource_id = format!("db-{}", Uuid::new_v4().simple());
9631 let inst = DbInstance {
9632 db_instance_identifier: identifier.clone(),
9633 db_instance_arn: arn.clone(),
9634 db_instance_class: class,
9635 engine,
9636 engine_version,
9637 db_instance_status: "available".to_string(),
9638 master_username,
9639 db_name,
9640 endpoint_address,
9641 port,
9642 allocated_storage,
9643 publicly_accessible,
9644 deletion_protection,
9645 created_at: Utc::now(),
9646 dbi_resource_id,
9647 master_user_password,
9648 container_id: String::new(),
9649 host_port: 0,
9650 tags,
9651 read_replica_source_db_instance_identifier: None,
9652 read_replica_db_instance_identifiers: Vec::new(),
9653 vpc_security_group_ids,
9654 db_parameter_group_name,
9655 backup_retention_period,
9656 preferred_backup_window: "03:00-04:00".to_string(),
9657 preferred_maintenance_window: None,
9658 latest_restorable_time: None,
9659 option_group_name,
9660 multi_az,
9661 pending_modified_values: None,
9662 availability_zone,
9663 storage_type,
9664 storage_encrypted,
9665 kms_key_id,
9666 iam_database_authentication_enabled,
9667 iops: props.get("Iops").and_then(|v| v.as_i64()).map(|n| n as i32),
9668 monitoring_interval: props
9669 .get("MonitoringInterval")
9670 .and_then(|v| v.as_i64())
9671 .map(|n| n as i32),
9672 monitoring_role_arn: props
9673 .get("MonitoringRoleArn")
9674 .and_then(|v| v.as_str())
9675 .map(String::from),
9676 performance_insights_enabled: props
9677 .get("EnablePerformanceInsights")
9678 .and_then(|v| v.as_bool())
9679 .unwrap_or(false),
9680 performance_insights_kms_key_id: props
9681 .get("PerformanceInsightsKMSKeyId")
9682 .and_then(|v| v.as_str())
9683 .map(String::from),
9684 performance_insights_retention_period: props
9685 .get("PerformanceInsightsRetentionPeriod")
9686 .and_then(|v| v.as_i64())
9687 .map(|n| n as i32),
9688 enabled_cloudwatch_logs_exports,
9689 ca_certificate_identifier: props
9690 .get("CACertificateIdentifier")
9691 .and_then(|v| v.as_str())
9692 .map(String::from),
9693 network_type: props
9694 .get("NetworkType")
9695 .and_then(|v| v.as_str())
9696 .map(String::from),
9697 character_set_name: props
9698 .get("CharacterSetName")
9699 .and_then(|v| v.as_str())
9700 .map(String::from),
9701 auto_minor_version_upgrade: props
9702 .get("AutoMinorVersionUpgrade")
9703 .and_then(|v| v.as_bool()),
9704 copy_tags_to_snapshot: props.get("CopyTagsToSnapshot").and_then(|v| v.as_bool()),
9705 master_user_secret_arn: None,
9706 master_user_secret_kms_key_id: props
9707 .get("MasterUserSecret")
9708 .and_then(|v| v.get("KmsKeyId"))
9709 .and_then(|v| v.as_str())
9710 .map(String::from),
9711 license_model: props
9712 .get("LicenseModel")
9713 .and_then(|v| v.as_str())
9714 .map(String::from),
9715 max_allocated_storage: props
9716 .get("MaxAllocatedStorage")
9717 .and_then(|v| v.as_i64())
9718 .map(|n| n as i32),
9719 multi_tenant: props.get("MultiTenant").and_then(|v| v.as_bool()),
9720 storage_throughput: props
9721 .get("StorageThroughput")
9722 .and_then(|v| v.as_i64())
9723 .map(|n| n as i32),
9724 tde_credential_arn: props
9725 .get("TdeCredentialArn")
9726 .and_then(|v| v.as_str())
9727 .map(String::from),
9728 delete_automated_backups: props
9729 .get("DeleteAutomatedBackups")
9730 .and_then(|v| v.as_bool()),
9731 db_security_groups: props
9732 .get("DBSecurityGroups")
9733 .and_then(|v| v.as_array())
9734 .map(|arr| {
9735 arr.iter()
9736 .filter_map(|v| v.as_str().map(String::from))
9737 .collect()
9738 })
9739 .unwrap_or_default(),
9740 domain: props
9741 .get("Domain")
9742 .and_then(|v| v.as_str())
9743 .map(String::from),
9744 domain_fqdn: props
9745 .get("DomainFqdn")
9746 .and_then(|v| v.as_str())
9747 .map(String::from),
9748 domain_ou: props
9749 .get("DomainOu")
9750 .and_then(|v| v.as_str())
9751 .map(String::from),
9752 domain_iam_role_name: props
9753 .get("DomainIAMRoleName")
9754 .and_then(|v| v.as_str())
9755 .map(String::from),
9756 domain_auth_secret_arn: props
9757 .get("DomainAuthSecretArn")
9758 .and_then(|v| v.as_str())
9759 .map(String::from),
9760 domain_dns_ips: props
9761 .get("DomainDnsIps")
9762 .and_then(|v| v.as_array())
9763 .map(|arr| {
9764 arr.iter()
9765 .filter_map(|v| v.as_str().map(String::from))
9766 .collect()
9767 })
9768 .unwrap_or_default(),
9769 db_cluster_identifier: props
9770 .get("DBClusterIdentifier")
9771 .and_then(|v| v.as_str())
9772 .map(String::from),
9773 };
9774 let endpoint = inst.endpoint_address.clone();
9775 let endpoint_port = inst.port;
9776 state.instances.insert(identifier.clone(), inst);
9777
9778 Ok(ProvisionResult::new(identifier.clone())
9779 .with("DBInstanceArn", arn)
9780 .with("Endpoint.Address", endpoint)
9781 .with("Endpoint.Port", endpoint_port.to_string())
9782 .with("DbiResourceId", format!("db-{identifier}")))
9783 }
9784
9785 fn delete_rds_db_instance(&self, physical_id: &str) -> Result<(), String> {
9786 let mut accounts = self.rds_state.write();
9787 let state = accounts.get_or_create(&self.account_id);
9788 state.instances.remove(physical_id);
9789 Ok(())
9790 }
9791
9792 fn create_rds_db_cluster(
9793 &self,
9794 resource: &ResourceDefinition,
9795 ) -> Result<ProvisionResult, String> {
9796 let props = &resource.properties;
9797 let identifier = props
9798 .get("DBClusterIdentifier")
9799 .and_then(|v| v.as_str())
9800 .map(String::from)
9801 .unwrap_or_else(|| {
9802 format!(
9803 "cfn-cluster-{}-{}",
9804 resource.logical_id.to_lowercase(),
9805 Uuid::new_v4().simple().to_string()[..8].to_lowercase()
9806 )
9807 });
9808 let engine = props
9809 .get("Engine")
9810 .and_then(|v| v.as_str())
9811 .unwrap_or("aurora-postgresql")
9812 .to_string();
9813 let engine_version = props
9814 .get("EngineVersion")
9815 .and_then(|v| v.as_str())
9816 .map(String::from);
9817 let master_username = props
9818 .get("MasterUsername")
9819 .and_then(|v| v.as_str())
9820 .map(String::from);
9821 let port = props.get("Port").and_then(|v| v.as_i64()).unwrap_or(5432);
9822 let mut accounts = self.rds_state.write();
9823 let state = accounts.get_or_create(&self.account_id);
9824 let arn = format!(
9825 "arn:aws:rds:{}:{}:cluster:{}",
9826 state.region, state.account_id, identifier
9827 );
9828 let cluster_resource_id = format!("cluster-{}", Uuid::new_v4().simple());
9829 let endpoint = format!(
9830 "{identifier}.cluster-fakecloud.{}.rds.amazonaws.com",
9831 state.region
9832 );
9833 let reader_endpoint = format!(
9834 "{identifier}.cluster-ro-fakecloud.{}.rds.amazonaws.com",
9835 state.region
9836 );
9837 let body = serde_json::json!({
9838 "DBClusterIdentifier": identifier,
9839 "DBClusterArn": arn,
9840 "Engine": engine,
9841 "EngineVersion": engine_version,
9842 "MasterUsername": master_username,
9843 "Status": "available",
9844 "DbClusterResourceId": cluster_resource_id,
9845 "Endpoint": endpoint,
9846 "ReaderEndpoint": reader_endpoint,
9847 "Port": port,
9848 "AllocatedStorage": props.get("AllocatedStorage").and_then(|v| v.as_i64()).unwrap_or(1),
9849 "BackupRetentionPeriod": props.get("BackupRetentionPeriod").and_then(|v| v.as_i64()).unwrap_or(1),
9850 "DatabaseName": props.get("DatabaseName").and_then(|v| v.as_str()),
9851 "DBSubnetGroup": props.get("DBSubnetGroupName").and_then(|v| v.as_str()),
9852 "VpcSecurityGroupIds": props.get("VpcSecurityGroupIds").cloned().unwrap_or(serde_json::json!([])),
9853 "StorageEncrypted": props.get("StorageEncrypted").and_then(|v| v.as_bool()).unwrap_or(false),
9854 "KmsKeyId": props.get("KmsKeyId").and_then(|v| v.as_str()),
9855 "DeletionProtection": props.get("DeletionProtection").and_then(|v| v.as_bool()).unwrap_or(false),
9856 "ClusterCreateTime": Utc::now().to_rfc3339(),
9857 "EnabledCloudwatchLogsExports": props.get("EnableCloudwatchLogsExports").cloned().unwrap_or(serde_json::json!([])),
9858 "MultiAZ": false,
9859 "DBClusterMembers": [],
9860 });
9861 state
9862 .extras
9863 .entry("clusters".to_string())
9864 .or_default()
9865 .insert(identifier.clone(), body);
9866 Ok(ProvisionResult::new(identifier.clone())
9867 .with("DBClusterArn", arn)
9868 .with("Endpoint.Address", endpoint)
9869 .with("ReadEndpoint.Address", reader_endpoint)
9870 .with("Endpoint.Port", port.to_string())
9871 .with("DBClusterResourceId", cluster_resource_id))
9872 }
9873
9874 fn delete_rds_db_cluster(&self, physical_id: &str) -> Result<(), String> {
9875 let mut accounts = self.rds_state.write();
9876 let state = accounts.get_or_create(&self.account_id);
9877 if let Some(m) = state.extras.get_mut("clusters") {
9878 m.remove(physical_id);
9879 }
9880 Ok(())
9881 }
9882
9883 fn create_ecs_cluster(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
9886 let props = &resource.properties;
9887 let cluster_name = props
9888 .get("ClusterName")
9889 .and_then(|v| v.as_str())
9890 .unwrap_or(&resource.logical_id)
9891 .to_string();
9892 let cluster_arn = format!(
9893 "arn:aws:ecs:{}:{}:cluster/{}",
9894 self.region, self.account_id, cluster_name
9895 );
9896 let mut cluster = EcsCluster::new(&cluster_name, cluster_arn.clone());
9897 cluster.tags = parse_ecs_tags(props.get("Tags"));
9898 cluster.capacity_providers = props
9899 .get("CapacityProviders")
9900 .and_then(|v| v.as_array())
9901 .map(|arr| {
9902 arr.iter()
9903 .filter_map(|v| v.as_str().map(|s| s.to_string()))
9904 .collect()
9905 })
9906 .unwrap_or_default();
9907 if let Some(strategy) = props
9908 .get("DefaultCapacityProviderStrategy")
9909 .and_then(|v| v.as_array())
9910 {
9911 cluster.default_capacity_provider_strategy =
9912 strategy.iter().cloned().map(lowercase_first_keys).collect();
9913 }
9914 if let Some(cfg) = props.get("Configuration") {
9915 cluster.configuration = Some(lowercase_first_keys(cfg.clone()));
9916 }
9917 if let Some(settings) = props.get("ClusterSettings").and_then(|v| v.as_array()) {
9918 cluster.settings = settings.iter().cloned().map(lowercase_first_keys).collect();
9919 }
9920 if let Some(scd) = props.get("ServiceConnectDefaults") {
9921 cluster.service_connect_defaults = Some(lowercase_first_keys(scd.clone()));
9922 }
9923 let mut accounts = self.ecs_state.write();
9924 let state = accounts.get_or_create(&self.account_id);
9925 state.clusters.insert(cluster_name.clone(), cluster);
9926 Ok(ProvisionResult::new(cluster_name).with("Arn", cluster_arn))
9927 }
9928
9929 fn delete_ecs_cluster(&self, physical_id: &str) -> Result<(), String> {
9930 let mut accounts = self.ecs_state.write();
9931 let state = accounts.get_or_create(&self.account_id);
9932 state.clusters.remove(physical_id);
9933 state.services.retain(|_, s| s.cluster_name != physical_id);
9935 state
9936 .tasks
9937 .retain(|_, t| t.cluster_arn.split('/').next_back() != Some(physical_id));
9938 Ok(())
9939 }
9940
9941 fn create_ecs_task_definition(
9942 &self,
9943 resource: &ResourceDefinition,
9944 ) -> Result<ProvisionResult, String> {
9945 let props = &resource.properties;
9946 let family = props
9947 .get("Family")
9948 .and_then(|v| v.as_str())
9949 .unwrap_or(&resource.logical_id)
9950 .to_string();
9951 let container_definitions: Vec<serde_json::Value> = props
9954 .get("ContainerDefinitions")
9955 .and_then(|v| v.as_array())
9956 .cloned()
9957 .unwrap_or_default()
9958 .into_iter()
9959 .map(lowercase_first_keys)
9960 .collect();
9961 let task_role_arn = props
9962 .get("TaskRoleArn")
9963 .and_then(|v| v.as_str())
9964 .map(|s| s.to_string());
9965 let execution_role_arn = props
9966 .get("ExecutionRoleArn")
9967 .and_then(|v| v.as_str())
9968 .map(|s| s.to_string());
9969 let network_mode = props
9970 .get("NetworkMode")
9971 .and_then(|v| v.as_str())
9972 .map(|s| s.to_string());
9973 let requires_compatibilities: Vec<String> = props
9974 .get("RequiresCompatibilities")
9975 .and_then(|v| v.as_array())
9976 .map(|arr| {
9977 arr.iter()
9978 .filter_map(|v| v.as_str().map(|s| s.to_string()))
9979 .collect()
9980 })
9981 .unwrap_or_default();
9982 let cpu = props
9983 .get("Cpu")
9984 .and_then(|v| v.as_str())
9985 .map(|s| s.to_string());
9986 let memory = props
9987 .get("Memory")
9988 .and_then(|v| v.as_str())
9989 .map(|s| s.to_string());
9990 let volumes: Vec<serde_json::Value> = props
9991 .get("Volumes")
9992 .and_then(|v| v.as_array())
9993 .cloned()
9994 .unwrap_or_default()
9995 .into_iter()
9996 .map(lowercase_first_keys)
9997 .collect();
9998 let placement_constraints: Vec<serde_json::Value> = props
9999 .get("PlacementConstraints")
10000 .and_then(|v| v.as_array())
10001 .cloned()
10002 .unwrap_or_default()
10003 .into_iter()
10004 .map(lowercase_first_keys)
10005 .collect();
10006 let proxy_configuration = props
10007 .get("ProxyConfiguration")
10008 .cloned()
10009 .map(lowercase_first_keys);
10010 let ephemeral_storage = props
10011 .get("EphemeralStorage")
10012 .cloned()
10013 .map(lowercase_first_keys);
10014 let runtime_platform = props
10015 .get("RuntimePlatform")
10016 .cloned()
10017 .map(lowercase_first_keys);
10018 let tags = parse_ecs_tags(props.get("Tags"));
10019
10020 let mut accounts = self.ecs_state.write();
10021 let state = accounts.get_or_create(&self.account_id);
10022 let revision = state
10023 .next_revision
10024 .entry(family.clone())
10025 .and_modify(|n| *n += 1)
10026 .or_insert(1);
10027 let revision = *revision;
10028 let arn = format!(
10029 "arn:aws:ecs:{}:{}:task-definition/{}:{}",
10030 self.region, self.account_id, family, revision
10031 );
10032 let td = EcsTaskDefinition {
10033 family: family.clone(),
10034 revision,
10035 task_definition_arn: arn.clone(),
10036 container_definitions,
10037 status: "ACTIVE".to_string(),
10038 task_role_arn,
10039 execution_role_arn,
10040 network_mode,
10041 requires_compatibilities: requires_compatibilities.clone(),
10042 compatibilities: requires_compatibilities,
10043 cpu,
10044 memory,
10045 pid_mode: None,
10046 ipc_mode: None,
10047 volumes,
10048 placement_constraints,
10049 proxy_configuration,
10050 inference_accelerators: Vec::new(),
10051 ephemeral_storage,
10052 runtime_platform,
10053 requires_attributes: Vec::new(),
10054 registered_at: Utc::now(),
10055 registered_by: None,
10056 deregistered_at: None,
10057 tags,
10058 enable_fault_injection: props.get("EnableFaultInjection").and_then(|v| v.as_bool()),
10059 };
10060 state
10061 .task_definitions
10062 .entry(family.clone())
10063 .or_default()
10064 .insert(revision, td);
10065 Ok(ProvisionResult::new(arn.clone()).with("TaskDefinitionArn", arn))
10066 }
10067
10068 fn delete_ecs_task_definition(&self, physical_id: &str) -> Result<(), String> {
10069 let Some(suffix) = physical_id.rsplit('/').next() else {
10072 return Ok(());
10073 };
10074 let Some((family, rev)) = suffix.split_once(':') else {
10075 return Ok(());
10076 };
10077 let Ok(revision) = rev.parse::<i32>() else {
10078 return Ok(());
10079 };
10080 let mut accounts = self.ecs_state.write();
10081 let state = accounts.get_or_create(&self.account_id);
10082 if let Some(revs) = state.task_definitions.get_mut(family) {
10083 if let Some(td) = revs.get_mut(&revision) {
10084 td.status = "INACTIVE".to_string();
10085 td.deregistered_at = Some(Utc::now());
10086 }
10087 }
10088 Ok(())
10089 }
10090
10091 fn create_ecs_service(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
10092 let props = &resource.properties;
10093 let service_name = props
10094 .get("ServiceName")
10095 .and_then(|v| v.as_str())
10096 .unwrap_or(&resource.logical_id)
10097 .to_string();
10098 let cluster_name = props
10100 .get("Cluster")
10101 .and_then(|v| v.as_str())
10102 .map(parse_ecs_cluster_name)
10103 .unwrap_or_else(|| "default".to_string());
10104 let task_definition_arn = props
10105 .get("TaskDefinition")
10106 .and_then(|v| v.as_str())
10107 .ok_or_else(|| "TaskDefinition is required".to_string())?
10108 .to_string();
10109 let desired_count = props
10110 .get("DesiredCount")
10111 .and_then(cfn_as_i64)
10112 .map(|n| n as i32)
10113 .unwrap_or(1);
10114 let launch_type = props
10115 .get("LaunchType")
10116 .and_then(|v| v.as_str())
10117 .unwrap_or("FARGATE")
10118 .to_string();
10119 let scheduling_strategy = props
10120 .get("SchedulingStrategy")
10121 .and_then(|v| v.as_str())
10122 .unwrap_or("REPLICA")
10123 .to_string();
10124 let deployment_controller = props
10125 .get("DeploymentController")
10126 .and_then(|v| v.get("Type"))
10127 .and_then(|v| v.as_str())
10128 .unwrap_or("ECS")
10129 .to_string();
10130 let load_balancers: Vec<serde_json::Value> = props
10131 .get("LoadBalancers")
10132 .and_then(|v| v.as_array())
10133 .cloned()
10134 .unwrap_or_default()
10135 .into_iter()
10136 .map(lowercase_first_keys)
10137 .collect();
10138 let service_registries: Vec<serde_json::Value> = props
10139 .get("ServiceRegistries")
10140 .and_then(|v| v.as_array())
10141 .cloned()
10142 .unwrap_or_default()
10143 .into_iter()
10144 .map(lowercase_first_keys)
10145 .collect();
10146 let placement_constraints: Vec<serde_json::Value> = props
10147 .get("PlacementConstraints")
10148 .and_then(|v| v.as_array())
10149 .cloned()
10150 .unwrap_or_default()
10151 .into_iter()
10152 .map(lowercase_first_keys)
10153 .collect();
10154 let placement_strategy: Vec<serde_json::Value> = props
10155 .get("PlacementStrategies")
10156 .and_then(|v| v.as_array())
10157 .cloned()
10158 .unwrap_or_default()
10159 .into_iter()
10160 .map(lowercase_first_keys)
10161 .collect();
10162 let network_configuration = props
10163 .get("NetworkConfiguration")
10164 .cloned()
10165 .map(lowercase_first_keys);
10166 let role_arn = props
10167 .get("Role")
10168 .and_then(|v| v.as_str())
10169 .map(|s| s.to_string());
10170 let platform_version = props
10171 .get("PlatformVersion")
10172 .and_then(|v| v.as_str())
10173 .map(|s| s.to_string());
10174 let health_check_grace_period_seconds = props
10175 .get("HealthCheckGracePeriodSeconds")
10176 .and_then(cfn_as_i64)
10177 .map(|n| n as i32);
10178 let enable_ecs_managed_tags = props
10179 .get("EnableECSManagedTags")
10180 .and_then(|v| v.as_bool())
10181 .unwrap_or(false);
10182 let enable_execute_command = props
10183 .get("EnableExecuteCommand")
10184 .and_then(|v| v.as_bool())
10185 .unwrap_or(false);
10186 let propagate_tags = props
10187 .get("PropagateTags")
10188 .and_then(|v| v.as_str())
10189 .map(|s| s.to_string());
10190 let capacity_provider_strategy: Vec<serde_json::Value> = props
10191 .get("CapacityProviderStrategy")
10192 .and_then(|v| v.as_array())
10193 .cloned()
10194 .unwrap_or_default()
10195 .into_iter()
10196 .map(lowercase_first_keys)
10197 .collect();
10198 let availability_zone_rebalancing = props
10199 .get("AvailabilityZoneRebalancing")
10200 .and_then(|v| v.as_str())
10201 .map(|s| s.to_string());
10202 let tags = parse_ecs_tags(props.get("Tags"));
10203
10204 let (family, revision) = parse_td_arn(&task_definition_arn);
10206
10207 let mut accounts = self.ecs_state.write();
10208 let state = accounts.get_or_create(&self.account_id);
10209 if !state.clusters.contains_key(&cluster_name) {
10210 return Err(format!(
10211 "Cluster {cluster_name} does not exist yet — retry once it has been provisioned"
10212 ));
10213 }
10214 let cluster_arn = state.clusters[&cluster_name].cluster_arn.clone();
10215 let service_arn = format!(
10216 "arn:aws:ecs:{}:{}:service/{}/{}",
10217 self.region, self.account_id, cluster_name, service_name
10218 );
10219 let key = format!("{cluster_name}/{service_name}");
10220 let service = EcsService {
10221 service_name: service_name.clone(),
10222 service_arn: service_arn.clone(),
10223 cluster_name: cluster_name.clone(),
10224 cluster_arn,
10225 task_definition_arn,
10226 family,
10227 revision,
10228 desired_count,
10229 running_count: 0,
10230 pending_count: 0,
10231 launch_type,
10232 status: "ACTIVE".to_string(),
10233 scheduling_strategy,
10234 deployment_controller,
10235 minimum_healthy_percent: props
10236 .get("DeploymentConfiguration")
10237 .and_then(|v| v.get("MinimumHealthyPercent"))
10238 .and_then(cfn_as_i64)
10239 .map(|n| n as i32),
10240 maximum_percent: props
10241 .get("DeploymentConfiguration")
10242 .and_then(|v| v.get("MaximumPercent"))
10243 .and_then(cfn_as_i64)
10244 .map(|n| n as i32),
10245 circuit_breaker: None,
10246 deployments: Vec::new(),
10247 load_balancers,
10248 service_registries,
10249 placement_constraints,
10250 placement_strategy,
10251 network_configuration,
10252 tags,
10253 created_at: Utc::now(),
10254 created_by: None,
10255 role_arn,
10256 platform_version,
10257 health_check_grace_period_seconds,
10258 enable_execute_command,
10259 enable_ecs_managed_tags,
10260 propagate_tags,
10261 capacity_provider_strategy,
10262 availability_zone_rebalancing,
10263 volume_configurations: Vec::new(),
10264 };
10265 state.services.insert(key.clone(), service);
10266 if let Some(c) = state.clusters.get_mut(&cluster_name) {
10267 c.active_services_count += 1;
10268 }
10269 Ok(ProvisionResult::new(service_arn.clone())
10270 .with("Name", service_name)
10271 .with("ServiceArn", service_arn))
10272 }
10273
10274 fn delete_ecs_service(&self, physical_id: &str) -> Result<(), String> {
10275 let Some((cluster, service)) = parse_service_arn(physical_id) else {
10277 return Ok(());
10278 };
10279 let key = format!("{cluster}/{service}");
10280 let mut accounts = self.ecs_state.write();
10281 let state = accounts.get_or_create(&self.account_id);
10282 if state.services.remove(&key).is_some() {
10283 if let Some(c) = state.clusters.get_mut(&cluster) {
10284 if c.active_services_count > 0 {
10285 c.active_services_count -= 1;
10286 }
10287 }
10288 }
10289 Ok(())
10290 }
10291
10292 fn create_ecs_capacity_provider(
10293 &self,
10294 resource: &ResourceDefinition,
10295 ) -> Result<ProvisionResult, String> {
10296 let props = &resource.properties;
10297 let name = props
10298 .get("Name")
10299 .and_then(|v| v.as_str())
10300 .unwrap_or(&resource.logical_id)
10301 .to_string();
10302 let arn = format!(
10303 "arn:aws:ecs:{}:{}:capacity-provider/{}",
10304 self.region, self.account_id, name
10305 );
10306 let cp = EcsCapacityProvider {
10307 name: name.clone(),
10308 arn: arn.clone(),
10309 status: "ACTIVE".to_string(),
10310 auto_scaling_group_provider: props.get("AutoScalingGroupProvider").cloned(),
10311 update_status: None,
10312 update_status_reason: None,
10313 created_at: Utc::now(),
10314 tags: parse_ecs_tags(props.get("Tags")),
10315 };
10316 let mut accounts = self.ecs_state.write();
10317 let state = accounts.get_or_create(&self.account_id);
10318 state.capacity_providers.insert(name.clone(), cp);
10319 Ok(ProvisionResult::new(name).with("Arn", arn))
10320 }
10321
10322 fn delete_ecs_capacity_provider(&self, physical_id: &str) -> Result<(), String> {
10323 let mut accounts = self.ecs_state.write();
10324 let state = accounts.get_or_create(&self.account_id);
10325 state.capacity_providers.remove(physical_id);
10326 Ok(())
10327 }
10328
10329 fn update_ecs_cluster(
10334 &self,
10335 existing: &StackResource,
10336 resource: &ResourceDefinition,
10337 ) -> Result<ProvisionResult, String> {
10338 let props = &resource.properties;
10339 let cluster_name = existing.physical_id.clone();
10340 let mut accounts = self.ecs_state.write();
10341 let state = accounts.get_or_create(&self.account_id);
10342 let cluster = state
10343 .clusters
10344 .get_mut(&cluster_name)
10345 .ok_or_else(|| format!("Cluster {cluster_name} no longer exists"))?;
10346 if let Some(settings) = props.get("ClusterSettings").and_then(|v| v.as_array()) {
10347 cluster.settings = settings.iter().cloned().map(lowercase_first_keys).collect();
10348 }
10349 if let Some(cfg) = props.get("Configuration") {
10350 cluster.configuration = Some(lowercase_first_keys(cfg.clone()));
10351 }
10352 if let Some(cps) = props.get("CapacityProviders").and_then(|v| v.as_array()) {
10353 cluster.capacity_providers = cps
10354 .iter()
10355 .filter_map(|v| v.as_str().map(|s| s.to_string()))
10356 .collect();
10357 }
10358 if let Some(strategy) = props
10359 .get("DefaultCapacityProviderStrategy")
10360 .and_then(|v| v.as_array())
10361 {
10362 cluster.default_capacity_provider_strategy =
10363 strategy.iter().cloned().map(lowercase_first_keys).collect();
10364 }
10365 if let Some(scd) = props.get("ServiceConnectDefaults") {
10366 cluster.service_connect_defaults = Some(lowercase_first_keys(scd.clone()));
10367 }
10368 if props.get("Tags").is_some() {
10369 cluster.tags = parse_ecs_tags(props.get("Tags"));
10370 }
10371 let cluster_arn = cluster.cluster_arn.clone();
10372 Ok(ProvisionResult::new(cluster_name).with("Arn", cluster_arn))
10373 }
10374
10375 fn update_ecs_service(
10380 &self,
10381 existing: &StackResource,
10382 resource: &ResourceDefinition,
10383 ) -> Result<ProvisionResult, String> {
10384 let props = &resource.properties;
10385 let service_arn = existing.physical_id.clone();
10386 let Some((cluster_name, service_name)) = parse_service_arn(&service_arn) else {
10387 return Err(format!("Cannot parse service ARN: {service_arn}"));
10388 };
10389 let key = format!("{cluster_name}/{service_name}");
10390 let mut accounts = self.ecs_state.write();
10391 let state = accounts.get_or_create(&self.account_id);
10392 let svc = state
10393 .services
10394 .get_mut(&key)
10395 .ok_or_else(|| format!("Service {service_arn} no longer exists"))?;
10396 if let Some(td) = props.get("TaskDefinition").and_then(|v| v.as_str()) {
10397 svc.task_definition_arn = td.to_string();
10398 let (family, revision) = parse_td_arn(td);
10399 svc.family = family;
10400 svc.revision = revision;
10401 }
10402 if let Some(n) = props.get("DesiredCount").and_then(cfn_as_i64) {
10403 svc.desired_count = n as i32;
10404 }
10405 if let Some(s) = props.get("LaunchType").and_then(|v| v.as_str()) {
10406 svc.launch_type = s.to_string();
10407 }
10408 if let Some(s) = props.get("PlatformVersion").and_then(|v| v.as_str()) {
10409 svc.platform_version = Some(s.to_string());
10410 }
10411 if let Some(n) = props
10412 .get("HealthCheckGracePeriodSeconds")
10413 .and_then(cfn_as_i64)
10414 {
10415 svc.health_check_grace_period_seconds = Some(n as i32);
10416 }
10417 if let Some(b) = props.get("EnableExecuteCommand").and_then(|v| v.as_bool()) {
10418 svc.enable_execute_command = b;
10419 }
10420 if let Some(b) = props.get("EnableECSManagedTags").and_then(|v| v.as_bool()) {
10421 svc.enable_ecs_managed_tags = b;
10422 }
10423 if let Some(s) = props.get("PropagateTags").and_then(|v| v.as_str()) {
10424 svc.propagate_tags = Some(s.to_string());
10425 }
10426 if let Some(s) = props
10427 .get("AvailabilityZoneRebalancing")
10428 .and_then(|v| v.as_str())
10429 {
10430 svc.availability_zone_rebalancing = Some(s.to_string());
10431 }
10432 if let Some(arr) = props
10433 .get("CapacityProviderStrategy")
10434 .and_then(|v| v.as_array())
10435 {
10436 svc.capacity_provider_strategy =
10437 arr.iter().cloned().map(lowercase_first_keys).collect();
10438 }
10439 if let Some(dc) = props.get("DeploymentConfiguration") {
10440 if let Some(n) = dc.get("MinimumHealthyPercent").and_then(cfn_as_i64) {
10441 svc.minimum_healthy_percent = Some(n as i32);
10442 }
10443 if let Some(n) = dc.get("MaximumPercent").and_then(cfn_as_i64) {
10444 svc.maximum_percent = Some(n as i32);
10445 }
10446 }
10447 if let Some(arr) = props.get("LoadBalancers").and_then(|v| v.as_array()) {
10448 svc.load_balancers = arr.iter().cloned().map(lowercase_first_keys).collect();
10449 }
10450 if let Some(arr) = props.get("ServiceRegistries").and_then(|v| v.as_array()) {
10451 svc.service_registries = arr.iter().cloned().map(lowercase_first_keys).collect();
10452 }
10453 if let Some(arr) = props.get("PlacementConstraints").and_then(|v| v.as_array()) {
10454 svc.placement_constraints = arr.iter().cloned().map(lowercase_first_keys).collect();
10455 }
10456 if let Some(arr) = props.get("PlacementStrategies").and_then(|v| v.as_array()) {
10457 svc.placement_strategy = arr.iter().cloned().map(lowercase_first_keys).collect();
10458 }
10459 if let Some(nc) = props.get("NetworkConfiguration") {
10460 svc.network_configuration = Some(lowercase_first_keys(nc.clone()));
10461 }
10462 if props.get("Tags").is_some() {
10463 svc.tags = parse_ecs_tags(props.get("Tags"));
10464 }
10465 let name = svc.service_name.clone();
10466 Ok(ProvisionResult::new(service_arn.clone())
10467 .with("Name", name)
10468 .with("ServiceArn", service_arn))
10469 }
10470
10471 fn update_ecs_task_definition(
10476 &self,
10477 _existing: &StackResource,
10478 resource: &ResourceDefinition,
10479 ) -> Result<ProvisionResult, String> {
10480 self.create_ecs_task_definition(resource)
10484 }
10485
10486 fn update_ecs_capacity_provider(
10489 &self,
10490 existing: &StackResource,
10491 resource: &ResourceDefinition,
10492 ) -> Result<ProvisionResult, String> {
10493 let props = &resource.properties;
10494 let name = existing.physical_id.clone();
10495 let mut accounts = self.ecs_state.write();
10496 let state = accounts.get_or_create(&self.account_id);
10497 let cp = state
10498 .capacity_providers
10499 .get_mut(&name)
10500 .ok_or_else(|| format!("CapacityProvider {name} no longer exists"))?;
10501 if props.get("AutoScalingGroupProvider").is_some() {
10502 cp.auto_scaling_group_provider = props.get("AutoScalingGroupProvider").cloned();
10503 }
10504 if props.get("Tags").is_some() {
10505 cp.tags = parse_ecs_tags(props.get("Tags"));
10506 }
10507 let arn = cp.arn.clone();
10508 Ok(ProvisionResult::new(name).with("Arn", arn))
10509 }
10510
10511 fn get_att_ecs_cluster(&self, physical_id: &str, attribute: &str) -> Option<String> {
10512 let mut accounts = self.ecs_state.write();
10513 let state = accounts.get_or_create(&self.account_id);
10514 let cluster = state.clusters.get(physical_id)?;
10515 match attribute {
10516 "Arn" => Some(cluster.cluster_arn.clone()),
10517 _ => None,
10518 }
10519 }
10520
10521 fn get_att_ecs_service(&self, physical_id: &str, attribute: &str) -> Option<String> {
10522 let (cluster, service) = parse_service_arn(physical_id)?;
10523 let key = format!("{cluster}/{service}");
10524 let mut accounts = self.ecs_state.write();
10525 let state = accounts.get_or_create(&self.account_id);
10526 let svc = state.services.get(&key)?;
10527 match attribute {
10528 "Name" => Some(svc.service_name.clone()),
10529 "ServiceArn" => Some(svc.service_arn.clone()),
10530 _ => None,
10531 }
10532 }
10533
10534 fn get_att_ecs_capacity_provider(&self, physical_id: &str, attribute: &str) -> Option<String> {
10535 let mut accounts = self.ecs_state.write();
10536 let state = accounts.get_or_create(&self.account_id);
10537 let cp = state.capacity_providers.get(physical_id)?;
10538 match attribute {
10539 "Arn" => Some(cp.arn.clone()),
10540 _ => None,
10541 }
10542 }
10543
10544 fn create_acm_certificate(
10547 &self,
10548 resource: &ResourceDefinition,
10549 ) -> Result<ProvisionResult, String> {
10550 let props = &resource.properties;
10551 let domain_name = props
10552 .get("DomainName")
10553 .and_then(|v| v.as_str())
10554 .ok_or_else(|| "DomainName is required".to_string())?
10555 .to_string();
10556 let sans: Vec<String> = props
10557 .get("SubjectAlternativeNames")
10558 .and_then(|v| v.as_array())
10559 .map(|arr| {
10560 arr.iter()
10561 .filter_map(|v| v.as_str().map(|s| s.to_string()))
10562 .collect()
10563 })
10564 .unwrap_or_default();
10565 let key_algorithm = props
10566 .get("KeyAlgorithm")
10567 .and_then(|v| v.as_str())
10568 .unwrap_or("RSA_2048")
10569 .to_string();
10570 let validation_method = props
10571 .get("ValidationMethod")
10572 .and_then(|v| v.as_str())
10573 .unwrap_or("DNS")
10574 .to_string();
10575 let ca_arn = props
10576 .get("CertificateAuthorityArn")
10577 .and_then(|v| v.as_str())
10578 .map(|s| s.to_string());
10579 let tags = parse_acm_tags(props.get("Tags"));
10580 let cert_transparency = props
10581 .get("CertificateTransparencyLoggingPreference")
10582 .and_then(|v| v.as_str())
10583 .unwrap_or("ENABLED")
10584 .to_string();
10585
10586 let arn = format!(
10588 "arn:aws:acm:{}:{}:certificate/{}",
10589 self.region,
10590 self.account_id,
10591 Uuid::new_v4()
10592 );
10593 let now = Utc::now();
10594
10595 let mut all_names = vec![domain_name.clone()];
10599 for s in &sans {
10600 if !all_names.contains(s) {
10601 all_names.push(s.clone());
10602 }
10603 }
10604 let (cert_pem, key_pem) = rcgen::generate_simple_self_signed(all_names.clone())
10605 .map(|c| (c.cert.pem(), c.key_pair.serialize_pem()))
10606 .map(|(c, k)| (Some(c), Some(k)))
10607 .unwrap_or((None, None));
10608
10609 let domain_validation: Vec<AcmDomainValidation> =
10617 synth_acm_domain_validation(&domain_name, &sans, &validation_method)
10618 .into_iter()
10619 .map(|mut dv| {
10620 dv.validation_status = "SUCCESS".to_string();
10621 dv
10622 })
10623 .collect();
10624 let renewal_summary = Some(AcmRenewalSummary {
10625 renewal_status: "PENDING_AUTO_RENEWAL".to_string(),
10626 domain_validation: domain_validation.clone(),
10627 renewal_status_reason: None,
10628 updated_at: now,
10629 });
10630 let cert = AcmStoredCertificate {
10631 arn: arn.clone(),
10632 domain_name: domain_name.clone(),
10633 subject_alternative_names: all_names,
10634 status: "ISSUED".to_string(),
10635 cert_type: "AMAZON_ISSUED".to_string(),
10636 certificate_pem: cert_pem,
10637 certificate_chain_pem: None,
10638 private_key_pem: key_pem,
10639 idempotency_token: None,
10640 serial: format!("{:032x}", Uuid::new_v4().as_u128()),
10641 subject: format!("CN={domain_name}"),
10642 issuer: "Amazon".to_string(),
10643 key_algorithm,
10644 signature_algorithm: "SHA256WITHRSA".to_string(),
10645 created_at: now,
10646 issued_at: Some(now),
10647 imported_at: None,
10648 revoked_at: None,
10649 revocation_reason: None,
10650 not_before: now,
10651 not_after: now + chrono::Duration::days(395),
10653 validation_method: Some(validation_method.clone()),
10654 domain_validation,
10655 options: AcmCertificateOptions {
10656 certificate_transparency_logging_preference: cert_transparency,
10657 export: "DISABLED".to_string(),
10658 },
10659 renewal_eligibility: "ELIGIBLE".to_string(),
10660 managed_by: None,
10661 certificate_authority_arn: ca_arn,
10662 tags,
10663 in_use_by: Vec::new(),
10664 describe_read_count: 0,
10665 failure_reason: None,
10666 renewal_summary,
10667 };
10668
10669 let mut accounts = self.acm_state.write();
10670 let account = accounts
10671 .accounts
10672 .entry(self.account_id.clone())
10673 .or_default();
10674 account.certificates.insert(arn.clone(), cert);
10675
10676 Ok(ProvisionResult::new(arn))
10677 }
10678
10679 fn delete_acm_certificate(&self, physical_id: &str) -> Result<(), String> {
10680 let mut accounts = self.acm_state.write();
10681 if let Some(account) = accounts.accounts.get_mut(&self.account_id) {
10682 account.certificates.remove(physical_id);
10683 }
10684 Ok(())
10685 }
10686
10687 fn create_acm_account(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
10691 let days = resource
10692 .properties
10693 .get("ExpiryEventsConfiguration")
10694 .and_then(|v| v.get("DaysBeforeExpiry"))
10695 .and_then(|v| v.as_i64())
10696 .map(|n| n as i32);
10697 let mut accounts = self.acm_state.write();
10698 let account = accounts
10699 .accounts
10700 .entry(self.account_id.clone())
10701 .or_default();
10702 account.account_config.expiry_events_days_before_expiry = days;
10703 Ok(ProvisionResult::new(format!(
10704 "acm-account-{}",
10705 self.account_id
10706 )))
10707 }
10708
10709 fn delete_acm_account(&self) -> Result<(), String> {
10713 let mut accounts = self.acm_state.write();
10714 if let Some(account) = accounts.accounts.get_mut(&self.account_id) {
10715 account.account_config.expiry_events_days_before_expiry = None;
10716 }
10717 Ok(())
10718 }
10719
10720 fn create_ec_parameter_group(
10723 &self,
10724 resource: &ResourceDefinition,
10725 ) -> Result<ProvisionResult, String> {
10726 let props = &resource.properties;
10727 let name = props
10728 .get("CacheParameterGroupName")
10729 .and_then(|v| v.as_str())
10730 .unwrap_or(&resource.logical_id)
10731 .to_string();
10732 let family = props
10733 .get("CacheParameterGroupFamily")
10734 .and_then(|v| v.as_str())
10735 .unwrap_or("redis7")
10736 .to_string();
10737 let description = props
10738 .get("Description")
10739 .and_then(|v| v.as_str())
10740 .unwrap_or("")
10741 .to_string();
10742 let arn = format!(
10743 "arn:aws:elasticache:{}:{}:parametergroup:{}",
10744 self.region, self.account_id, name
10745 );
10746 let group = CacheParameterGroup {
10747 cache_parameter_group_name: name.clone(),
10748 cache_parameter_group_family: family,
10749 description,
10750 is_global: false,
10751 arn: arn.clone(),
10752 };
10753 let mut accounts = self.elasticache_state.write();
10754 let state = accounts.get_or_create(&self.account_id);
10755 state
10757 .parameter_groups
10758 .retain(|p| p.cache_parameter_group_name != name);
10759 state.parameter_groups.push(group);
10760 Ok(ProvisionResult::new(name).with("Arn", arn))
10761 }
10762
10763 fn delete_ec_parameter_group(&self, physical_id: &str) -> Result<(), String> {
10764 let mut accounts = self.elasticache_state.write();
10765 let state = accounts.get_or_create(&self.account_id);
10766 state
10767 .parameter_groups
10768 .retain(|p| p.cache_parameter_group_name != physical_id);
10769 Ok(())
10770 }
10771
10772 fn create_ec_subnet_group(
10773 &self,
10774 resource: &ResourceDefinition,
10775 ) -> Result<ProvisionResult, String> {
10776 let props = &resource.properties;
10777 let name = props
10778 .get("CacheSubnetGroupName")
10779 .and_then(|v| v.as_str())
10780 .unwrap_or(&resource.logical_id)
10781 .to_string();
10782 let description = props
10783 .get("Description")
10784 .and_then(|v| v.as_str())
10785 .unwrap_or("")
10786 .to_string();
10787 let subnet_ids: Vec<String> = props
10788 .get("SubnetIds")
10789 .and_then(|v| v.as_array())
10790 .map(|arr| {
10791 arr.iter()
10792 .filter_map(|v| v.as_str().map(|s| s.to_string()))
10793 .collect()
10794 })
10795 .unwrap_or_default();
10796 let arn = format!(
10797 "arn:aws:elasticache:{}:{}:subnetgroup:{}",
10798 self.region, self.account_id, name
10799 );
10800 let group = CacheSubnetGroup {
10801 cache_subnet_group_name: name.clone(),
10802 cache_subnet_group_description: description,
10803 vpc_id: String::new(),
10804 subnet_ids,
10805 arn: arn.clone(),
10806 };
10807 let mut accounts = self.elasticache_state.write();
10808 let state = accounts.get_or_create(&self.account_id);
10809 state.subnet_groups.insert(name.clone(), group);
10810 Ok(ProvisionResult::new(name).with("Arn", arn))
10811 }
10812
10813 fn delete_ec_subnet_group(&self, physical_id: &str) -> Result<(), String> {
10814 let mut accounts = self.elasticache_state.write();
10815 let state = accounts.get_or_create(&self.account_id);
10816 state.subnet_groups.remove(physical_id);
10817 Ok(())
10818 }
10819
10820 fn create_ec_security_group(
10821 &self,
10822 resource: &ResourceDefinition,
10823 ) -> Result<ProvisionResult, String> {
10824 let props = &resource.properties;
10825 let name = props
10826 .get("CacheSecurityGroupName")
10827 .and_then(|v| v.as_str())
10828 .unwrap_or(&resource.logical_id)
10829 .to_string();
10830 let description = props
10831 .get("Description")
10832 .and_then(|v| v.as_str())
10833 .unwrap_or("")
10834 .to_string();
10835 let arn = format!(
10836 "arn:aws:elasticache:{}:{}:securitygroup:{}",
10837 self.region, self.account_id, name
10838 );
10839 let group = CacheSecurityGroup {
10840 cache_security_group_name: name.clone(),
10841 description,
10842 owner_id: self.account_id.clone(),
10843 arn: arn.clone(),
10844 ec2_security_groups: Vec::new(),
10845 };
10846 let mut accounts = self.elasticache_state.write();
10847 let state = accounts.get_or_create(&self.account_id);
10848 state.security_groups.insert(name.clone(), group);
10849 Ok(ProvisionResult::new(name).with("Arn", arn))
10850 }
10851
10852 fn delete_ec_security_group(&self, physical_id: &str) -> Result<(), String> {
10853 let mut accounts = self.elasticache_state.write();
10854 let state = accounts.get_or_create(&self.account_id);
10855 state.security_groups.remove(physical_id);
10856 Ok(())
10857 }
10858
10859 fn create_ec_user(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
10860 let props = &resource.properties;
10861 let user_id = props
10862 .get("UserId")
10863 .and_then(|v| v.as_str())
10864 .unwrap_or(&resource.logical_id)
10865 .to_string();
10866 let user_name = props
10867 .get("UserName")
10868 .and_then(|v| v.as_str())
10869 .unwrap_or(&user_id)
10870 .to_string();
10871 let engine = props
10872 .get("Engine")
10873 .and_then(|v| v.as_str())
10874 .unwrap_or("redis")
10875 .to_string();
10876 let access_string = props
10877 .get("AccessString")
10878 .and_then(|v| v.as_str())
10879 .unwrap_or("on ~* +@all")
10880 .to_string();
10881 let authentication_type = props
10882 .get("AuthenticationMode")
10883 .and_then(|v| v.get("Type"))
10884 .and_then(|v| v.as_str())
10885 .unwrap_or("no-password-required")
10886 .to_string();
10887 let arn = format!(
10888 "arn:aws:elasticache:{}:{}:user:{}",
10889 self.region, self.account_id, user_id
10890 );
10891 let user = EcUser {
10892 user_id: user_id.clone(),
10893 user_name,
10894 engine,
10895 access_string,
10896 status: "active".to_string(),
10897 authentication_type,
10898 password_count: 0,
10899 arn: arn.clone(),
10900 minimum_engine_version: "6.0".to_string(),
10901 user_group_ids: Vec::new(),
10902 };
10903 let mut accounts = self.elasticache_state.write();
10904 let state = accounts.get_or_create(&self.account_id);
10905 state.users.insert(user_id.clone(), user);
10906 Ok(ProvisionResult::new(user_id).with("Arn", arn))
10907 }
10908
10909 fn delete_ec_user(&self, physical_id: &str) -> Result<(), String> {
10910 let mut accounts = self.elasticache_state.write();
10911 let state = accounts.get_or_create(&self.account_id);
10912 state.users.remove(physical_id);
10913 Ok(())
10914 }
10915
10916 fn create_ec_user_group(
10917 &self,
10918 resource: &ResourceDefinition,
10919 ) -> Result<ProvisionResult, String> {
10920 let props = &resource.properties;
10921 let user_group_id = props
10922 .get("UserGroupId")
10923 .and_then(|v| v.as_str())
10924 .unwrap_or(&resource.logical_id)
10925 .to_string();
10926 let engine = props
10927 .get("Engine")
10928 .and_then(|v| v.as_str())
10929 .unwrap_or("redis")
10930 .to_string();
10931 let user_ids: Vec<String> = props
10932 .get("UserIds")
10933 .and_then(|v| v.as_array())
10934 .map(|arr| {
10935 arr.iter()
10936 .filter_map(|v| v.as_str().map(|s| s.to_string()))
10937 .collect()
10938 })
10939 .unwrap_or_default();
10940 let arn = format!(
10941 "arn:aws:elasticache:{}:{}:usergroup:{}",
10942 self.region, self.account_id, user_group_id
10943 );
10944 let group = EcUserGroup {
10945 user_group_id: user_group_id.clone(),
10946 engine,
10947 status: "active".to_string(),
10948 user_ids,
10949 arn: arn.clone(),
10950 minimum_engine_version: "6.0".to_string(),
10951 pending_changes: None,
10952 replication_groups: Vec::new(),
10953 };
10954 let mut accounts = self.elasticache_state.write();
10955 let state = accounts.get_or_create(&self.account_id);
10956 state.user_groups.insert(user_group_id.clone(), group);
10957 Ok(ProvisionResult::new(user_group_id).with("Arn", arn))
10958 }
10959
10960 fn delete_ec_user_group(&self, physical_id: &str) -> Result<(), String> {
10961 let mut accounts = self.elasticache_state.write();
10962 let state = accounts.get_or_create(&self.account_id);
10963 state.user_groups.remove(physical_id);
10964 Ok(())
10965 }
10966
10967 fn create_ec_cache_cluster(
10968 &self,
10969 resource: &ResourceDefinition,
10970 ) -> Result<ProvisionResult, String> {
10971 let props = &resource.properties;
10972 let id = props
10973 .get("ClusterName")
10974 .and_then(|v| v.as_str())
10975 .map(String::from)
10976 .unwrap_or_else(|| format!("cfn-cc-{}", resource.logical_id.to_lowercase()));
10977 let cache_node_type = props
10978 .get("CacheNodeType")
10979 .and_then(|v| v.as_str())
10980 .unwrap_or("cache.t4g.micro")
10981 .to_string();
10982 let engine = props
10983 .get("Engine")
10984 .and_then(|v| v.as_str())
10985 .unwrap_or("redis")
10986 .to_string();
10987 let engine_version = props
10988 .get("EngineVersion")
10989 .and_then(|v| v.as_str())
10990 .unwrap_or("7.1")
10991 .to_string();
10992 let num_cache_nodes = props
10993 .get("NumCacheNodes")
10994 .and_then(|v| v.as_i64())
10995 .map(|n| n as i32)
10996 .unwrap_or(1);
10997 let preferred_az = props
10998 .get("PreferredAvailabilityZone")
10999 .and_then(|v| v.as_str())
11000 .unwrap_or("us-east-1a")
11001 .to_string();
11002 let cache_subnet_group_name = props
11003 .get("CacheSubnetGroupName")
11004 .and_then(|v| v.as_str())
11005 .map(String::from);
11006 let auto_minor_version_upgrade = props
11007 .get("AutoMinorVersionUpgrade")
11008 .and_then(|v| v.as_bool())
11009 .unwrap_or(true);
11010 let default_port = if engine == "memcached" { 11211 } else { 6379 };
11011 let port = props
11012 .get("Port")
11013 .and_then(|v| v.as_i64())
11014 .map(|n| n as u16)
11015 .unwrap_or(default_port);
11016 let cache_parameter_group_name = props
11017 .get("CacheParameterGroupName")
11018 .and_then(|v| v.as_str())
11019 .map(String::from);
11020 let security_group_ids: Vec<String> = props
11021 .get("VpcSecurityGroupIds")
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 cache_security_group_names: Vec<String> = props
11030 .get("CacheSecurityGroupNames")
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 preferred_availability_zones: Vec<String> = props
11039 .get("PreferredAvailabilityZones")
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_arns: Vec<String> = props
11048 .get("SnapshotArns")
11049 .and_then(|v| v.as_array())
11050 .map(|arr| {
11051 arr.iter()
11052 .filter_map(|v| v.as_str().map(String::from))
11053 .collect()
11054 })
11055 .unwrap_or_default();
11056 let snapshot_name = props
11057 .get("SnapshotName")
11058 .and_then(|v| v.as_str())
11059 .map(String::from);
11060 let snapshot_retention_limit = props
11061 .get("SnapshotRetentionLimit")
11062 .and_then(|v| v.as_i64())
11063 .map(|n| n as i32)
11064 .unwrap_or(0);
11065 let snapshot_window = props
11066 .get("SnapshotWindow")
11067 .and_then(|v| v.as_str())
11068 .map(String::from);
11069 let preferred_maintenance_window = props
11070 .get("PreferredMaintenanceWindow")
11071 .and_then(|v| v.as_str())
11072 .map(String::from);
11073 let notification_topic_arn = props
11074 .get("NotificationTopicArn")
11075 .and_then(|v| v.as_str())
11076 .map(String::from);
11077 let transit_encryption_enabled = props
11078 .get("TransitEncryptionEnabled")
11079 .and_then(|v| v.as_bool())
11080 .unwrap_or(false);
11081 let auth_token = props
11082 .get("AuthToken")
11083 .and_then(|v| v.as_str())
11084 .filter(|s| !s.is_empty())
11085 .map(String::from);
11086 let auth_token_enabled = auth_token.is_some();
11087 let network_type = props
11088 .get("NetworkType")
11089 .and_then(|v| v.as_str())
11090 .map(String::from)
11091 .or_else(|| Some("ipv4".to_string()));
11092 let ip_discovery = props
11093 .get("IpDiscovery")
11094 .and_then(|v| v.as_str())
11095 .map(String::from)
11096 .or_else(|| Some("ipv4".to_string()));
11097 let az_mode = props
11098 .get("AZMode")
11099 .and_then(|v| v.as_str())
11100 .map(String::from)
11101 .or_else(|| Some("single-az".to_string()));
11102 let outpost_mode = props
11103 .get("OutpostMode")
11104 .and_then(|v| v.as_str())
11105 .map(String::from);
11106 let preferred_outpost_arn = props
11107 .get("PreferredOutpostArn")
11108 .and_then(|v| v.as_str())
11109 .map(String::from);
11110
11111 let mut accounts = self.elasticache_state.write();
11112 let state = accounts.get_or_create(&self.account_id);
11113 let arn = format!(
11114 "arn:aws:elasticache:{}:{}:cluster:{}",
11115 state.region, state.account_id, id
11116 );
11117 let endpoint_address = format!("{id}.fakecloud.{}.cache.amazonaws.com", state.region);
11118 let cluster = EcCacheCluster {
11119 cache_cluster_id: id.clone(),
11120 cache_node_type,
11121 engine,
11122 engine_version,
11123 cache_cluster_status: "available".to_string(),
11124 num_cache_nodes,
11125 preferred_availability_zone: preferred_az,
11126 cache_subnet_group_name,
11127 auto_minor_version_upgrade,
11128 arn: arn.clone(),
11129 created_at: Utc::now().to_rfc3339(),
11130 endpoint_address: endpoint_address.clone(),
11131 endpoint_port: port,
11132 container_id: String::new(),
11133 host_port: 0,
11134 replication_group_id: None,
11135 cache_parameter_group_name,
11136 security_group_ids,
11137 log_delivery_configurations: Vec::new(),
11138 transit_encryption_enabled,
11139 at_rest_encryption_enabled: false,
11140 auth_token_enabled,
11141 port,
11142 preferred_maintenance_window,
11143 preferred_availability_zones,
11144 notification_topic_arn,
11145 cache_security_group_names,
11146 snapshot_arns,
11147 snapshot_name,
11148 snapshot_retention_limit,
11149 snapshot_window,
11150 outpost_mode,
11151 preferred_outpost_arn,
11152 network_type,
11153 ip_discovery,
11154 az_mode,
11155 auth_token,
11156 kms_key_id: None,
11157 transit_encryption_mode: None,
11158 data_tiering_enabled: None,
11159 cluster_mode: None,
11160 preferred_outpost_arns: Vec::new(),
11161 };
11162 state.cache_clusters.insert(id.clone(), cluster);
11163 Ok(ProvisionResult::new(id.clone())
11164 .with("Arn", arn)
11165 .with("RedisEndpoint.Address", endpoint_address.clone())
11166 .with("RedisEndpoint.Port", port.to_string())
11167 .with("ConfigurationEndpoint.Address", endpoint_address)
11168 .with("ConfigurationEndpoint.Port", port.to_string()))
11169 }
11170
11171 fn delete_ec_cache_cluster(&self, physical_id: &str) -> Result<(), String> {
11172 let mut accounts = self.elasticache_state.write();
11173 let state = accounts.get_or_create(&self.account_id);
11174 state.cache_clusters.remove(physical_id);
11175 Ok(())
11176 }
11177
11178 fn create_ec_replication_group(
11179 &self,
11180 resource: &ResourceDefinition,
11181 ) -> Result<ProvisionResult, String> {
11182 let props = &resource.properties;
11183 let id = props
11184 .get("ReplicationGroupId")
11185 .and_then(|v| v.as_str())
11186 .map(String::from)
11187 .unwrap_or_else(|| format!("cfn-rg-{}", resource.logical_id.to_lowercase()));
11188 let description = props
11189 .get("ReplicationGroupDescription")
11190 .and_then(|v| v.as_str())
11191 .unwrap_or("CFN-provisioned replication group")
11192 .to_string();
11193 let cache_node_type = props
11194 .get("CacheNodeType")
11195 .and_then(|v| v.as_str())
11196 .unwrap_or("cache.t4g.micro")
11197 .to_string();
11198 let engine = props
11199 .get("Engine")
11200 .and_then(|v| v.as_str())
11201 .unwrap_or("redis")
11202 .to_string();
11203 let engine_version = props
11204 .get("EngineVersion")
11205 .and_then(|v| v.as_str())
11206 .unwrap_or("7.1")
11207 .to_string();
11208 let num_cache_clusters = props
11209 .get("NumCacheClusters")
11210 .and_then(|v| v.as_i64())
11211 .map(|n| n as i32)
11212 .unwrap_or(1);
11213 let num_node_groups = props
11214 .get("NumNodeGroups")
11215 .and_then(|v| v.as_i64())
11216 .map(|n| n as i32)
11217 .unwrap_or(0);
11218 let replicas_per_node_group = props
11219 .get("ReplicasPerNodeGroup")
11220 .and_then(|v| v.as_i64())
11221 .map(|n| n as i32);
11222 let automatic_failover_enabled = props
11223 .get("AutomaticFailoverEnabled")
11224 .and_then(|v| v.as_bool())
11225 .unwrap_or(false);
11226 let multi_az_enabled = props
11227 .get("MultiAZEnabled")
11228 .and_then(|v| v.as_bool())
11229 .unwrap_or(false);
11230 let transit_encryption_enabled = props
11231 .get("TransitEncryptionEnabled")
11232 .and_then(|v| v.as_bool())
11233 .unwrap_or(false);
11234 let at_rest_encryption_enabled = props
11235 .get("AtRestEncryptionEnabled")
11236 .and_then(|v| v.as_bool())
11237 .unwrap_or(false);
11238 let kms_key_id = props
11239 .get("KmsKeyId")
11240 .and_then(|v| v.as_str())
11241 .map(String::from);
11242 let auth_token_enabled = props
11243 .get("AuthToken")
11244 .and_then(|v| v.as_str())
11245 .filter(|s| !s.is_empty())
11246 .is_some();
11247 let user_group_ids: Vec<String> = props
11248 .get("UserGroupIds")
11249 .and_then(|v| v.as_array())
11250 .map(|arr| {
11251 arr.iter()
11252 .filter_map(|v| v.as_str().map(String::from))
11253 .collect()
11254 })
11255 .unwrap_or_default();
11256 let snapshot_retention_limit = props
11257 .get("SnapshotRetentionLimit")
11258 .and_then(|v| v.as_i64())
11259 .map(|n| n as i32)
11260 .unwrap_or(0);
11261 let snapshot_window = props
11262 .get("SnapshotWindow")
11263 .and_then(|v| v.as_str())
11264 .unwrap_or("00:00-01:00")
11265 .to_string();
11266 let port = props
11267 .get("Port")
11268 .and_then(|v| v.as_i64())
11269 .map(|n| n as u16)
11270 .unwrap_or(6379);
11271 let cluster_enabled = num_node_groups > 1
11272 || props
11273 .get("ClusterEnabled")
11274 .and_then(|v| v.as_bool())
11275 .unwrap_or(false);
11276
11277 let mut accounts = self.elasticache_state.write();
11278 let state = accounts.get_or_create(&self.account_id);
11279 let arn = format!(
11280 "arn:aws:elasticache:{}:{}:replicationgroup:{}",
11281 state.region, state.account_id, id
11282 );
11283 let endpoint_address = format!(
11284 "{id}.fakecloud.ng.0001.{}.cache.amazonaws.com",
11285 state.region
11286 );
11287 let configuration_endpoint = if cluster_enabled {
11288 Some(format!(
11289 "{id}.fakecloud.cfg.{}.cache.amazonaws.com",
11290 state.region
11291 ))
11292 } else {
11293 None
11294 };
11295
11296 let group = EcReplicationGroup {
11297 replication_group_id: id.clone(),
11298 description,
11299 global_replication_group_id: None,
11300 global_replication_group_role: None,
11301 status: "available".to_string(),
11302 cache_node_type,
11303 engine,
11304 engine_version,
11305 num_cache_clusters,
11306 automatic_failover_enabled,
11307 endpoint_address: endpoint_address.clone(),
11308 endpoint_port: port,
11309 arn: arn.clone(),
11310 created_at: Utc::now().to_rfc3339(),
11311 container_id: String::new(),
11312 host_port: 0,
11313 member_clusters: Vec::new(),
11314 snapshot_retention_limit,
11315 snapshot_window,
11316 transit_encryption_enabled,
11317 at_rest_encryption_enabled,
11318 cluster_enabled,
11319 kms_key_id,
11320 auth_token_enabled,
11321 user_group_ids,
11322 multi_az_enabled,
11323 log_delivery_configurations: Vec::new(),
11324 data_tiering: props
11325 .get("DataTieringEnabled")
11326 .and_then(|v| v.as_bool())
11327 .map(|b| if b { "enabled" } else { "disabled" }.to_string()),
11328 ip_discovery: props
11329 .get("IpDiscovery")
11330 .and_then(|v| v.as_str())
11331 .map(String::from),
11332 network_type: props
11333 .get("NetworkType")
11334 .and_then(|v| v.as_str())
11335 .map(String::from),
11336 transit_encryption_mode: props
11337 .get("TransitEncryptionMode")
11338 .and_then(|v| v.as_str())
11339 .map(String::from),
11340 num_node_groups,
11341 configuration_endpoint_address: configuration_endpoint.clone(),
11342 configuration_endpoint_port: configuration_endpoint.as_ref().map(|_| port),
11343 replicas_per_node_group,
11344 auth_token: props
11345 .get("AuthToken")
11346 .and_then(|v| v.as_str())
11347 .filter(|s| !s.is_empty())
11348 .map(String::from),
11349 port,
11350 notification_topic_arn: props
11351 .get("NotificationTopicArn")
11352 .and_then(|v| v.as_str())
11353 .map(String::from),
11354 cluster_mode: props
11355 .get("ClusterMode")
11356 .and_then(|v| v.as_str())
11357 .map(String::from),
11358 data_tiering_enabled: props.get("DataTieringEnabled").and_then(|v| v.as_bool()),
11359 notification_topic_status: None,
11360 cache_parameter_group_name: props
11361 .get("CacheParameterGroupName")
11362 .and_then(|v| v.as_str())
11363 .map(String::from),
11364 cache_subnet_group_name: props
11365 .get("CacheSubnetGroupName")
11366 .and_then(|v| v.as_str())
11367 .map(String::from),
11368 security_group_ids: props
11369 .get("SecurityGroupIds")
11370 .and_then(|v| v.as_array())
11371 .map(|arr| {
11372 arr.iter()
11373 .filter_map(|v| v.as_str().map(String::from))
11374 .collect()
11375 })
11376 .unwrap_or_default(),
11377 preferred_maintenance_window: props
11378 .get("PreferredMaintenanceWindow")
11379 .and_then(|v| v.as_str())
11380 .map(String::from),
11381 snapshot_name: props
11382 .get("SnapshotName")
11383 .and_then(|v| v.as_str())
11384 .map(String::from),
11385 snapshot_arns: props
11386 .get("SnapshotArns")
11387 .and_then(|v| v.as_array())
11388 .map(|arr| {
11389 arr.iter()
11390 .filter_map(|v| v.as_str().map(String::from))
11391 .collect()
11392 })
11393 .unwrap_or_default(),
11394 auto_minor_version_upgrade: props
11395 .get("AutoMinorVersionUpgrade")
11396 .and_then(|v| v.as_bool())
11397 .unwrap_or(true),
11398 };
11399 state.replication_groups.insert(id.clone(), group);
11400
11401 let mut result = ProvisionResult::new(id.clone())
11402 .with("Arn", arn)
11403 .with("PrimaryEndPoint.Address", endpoint_address.clone())
11404 .with("PrimaryEndPoint.Port", port.to_string())
11405 .with("ReadEndPoint.Addresses", endpoint_address.clone())
11406 .with("ReadEndPoint.Ports", port.to_string());
11407 if let Some(cfg) = configuration_endpoint {
11408 result = result
11409 .with("ConfigurationEndPoint.Address", cfg)
11410 .with("ConfigurationEndPoint.Port", port.to_string());
11411 }
11412 Ok(result)
11413 }
11414
11415 fn delete_ec_replication_group(&self, physical_id: &str) -> Result<(), String> {
11416 let mut accounts = self.elasticache_state.write();
11417 let state = accounts.get_or_create(&self.account_id);
11418 state.replication_groups.remove(physical_id);
11419 Ok(())
11420 }
11421
11422 fn create_route53_hosted_zone(
11425 &self,
11426 resource: &ResourceDefinition,
11427 ) -> Result<ProvisionResult, String> {
11428 let props = &resource.properties;
11429 let name = props
11430 .get("Name")
11431 .and_then(|v| v.as_str())
11432 .ok_or("Name is required")?
11433 .to_string();
11434 let normalized_name = if name.ends_with('.') {
11435 name.clone()
11436 } else {
11437 format!("{name}.")
11438 };
11439 let comment = props
11440 .get("HostedZoneConfig")
11441 .and_then(|v| v.get("Comment"))
11442 .and_then(|v| v.as_str())
11443 .map(|s| s.to_string());
11444 let private_zone = props
11445 .get("VPCs")
11446 .and_then(|v| v.as_array())
11447 .map(|a| !a.is_empty())
11448 .unwrap_or(false);
11449 let vpcs: Vec<fakecloud_route53::model::VPC> = props
11450 .get("VPCs")
11451 .and_then(|v| v.as_array())
11452 .map(|arr| {
11453 arr.iter()
11454 .map(|vpc| fakecloud_route53::model::VPC {
11455 vpc_id: vpc.get("VPCId").and_then(|v| v.as_str()).map(String::from),
11456 vpc_region: vpc
11457 .get("VPCRegion")
11458 .and_then(|v| v.as_str())
11459 .map(String::from),
11460 })
11461 .collect()
11462 })
11463 .unwrap_or_default();
11464
11465 let id = format!(
11466 "Z{}",
11467 Uuid::new_v4().simple().to_string()[..14].to_uppercase()
11468 );
11469 let name_servers = (1..=4)
11470 .map(|i| format!("ns-{}.awsdns-{:02}.com", 100 + i, i))
11471 .collect::<Vec<_>>();
11472
11473 let zone = StoredHostedZone {
11474 id: id.clone(),
11475 name: normalized_name,
11476 caller_reference: format!("cfn-{}", resource.logical_id),
11477 comment,
11478 private_zone,
11479 features: Some(HostedZoneFeatures::default()),
11480 vpcs,
11481 delegation_set_id: None,
11482 name_servers: name_servers.clone(),
11483 created_time: Utc::now(),
11484 resource_record_sets: Vec::new(),
11485 };
11486
11487 let mut accounts = self.route53_state.write();
11488 let state = accounts.entry("000000000000");
11491 state.hosted_zones.insert(id.clone(), zone);
11492
11493 let mut result = ProvisionResult::new(id.clone()).with("Id", id);
11494 for (i, ns) in name_servers.iter().enumerate() {
11495 result = result.with(&format!("NameServers.{i}"), ns.clone());
11496 }
11497 result = result.with("NameServers", name_servers.join(","));
11498 Ok(result)
11499 }
11500
11501 fn delete_route53_hosted_zone(&self, physical_id: &str) -> Result<(), String> {
11502 let mut accounts = self.route53_state.write();
11503 let state = accounts.entry("000000000000");
11506 state.hosted_zones.remove(physical_id);
11507 Ok(())
11508 }
11509
11510 fn create_route53_record_set(
11511 &self,
11512 resource: &ResourceDefinition,
11513 ) -> Result<ProvisionResult, String> {
11514 let props = &resource.properties;
11515 let zone_id = props
11516 .get("HostedZoneId")
11517 .and_then(|v| v.as_str())
11518 .ok_or_else(|| {
11519 "HostedZoneId is required (HostedZoneName lookups not supported)".to_string()
11520 })?
11521 .to_string();
11522 let name = props
11523 .get("Name")
11524 .and_then(|v| v.as_str())
11525 .ok_or("Name is required")?
11526 .to_string();
11527 let normalized_name = if name.ends_with('.') {
11528 name.clone()
11529 } else {
11530 format!("{name}.")
11531 };
11532 let record_type = props
11533 .get("Type")
11534 .and_then(|v| v.as_str())
11535 .ok_or("Type is required")?
11536 .to_string();
11537 let ttl = props.get("TTL").and_then(|v| {
11538 v.as_str()
11539 .and_then(|s| s.parse::<i64>().ok())
11540 .or_else(|| v.as_i64())
11541 });
11542 let resource_records = props
11543 .get("ResourceRecords")
11544 .and_then(|v| v.as_array())
11545 .map(|arr| {
11546 let recs: Vec<fakecloud_route53::model::ResourceRecord> = arr
11547 .iter()
11548 .filter_map(|v| {
11549 v.as_str()
11550 .map(|s| fakecloud_route53::model::ResourceRecord {
11551 value: s.to_string(),
11552 })
11553 })
11554 .collect();
11555 fakecloud_route53::model::ResourceRecords {
11556 resource_record: recs,
11557 }
11558 });
11559 let alias_target =
11560 props
11561 .get("AliasTarget")
11562 .map(|v| fakecloud_route53::model::AliasTarget {
11563 hosted_zone_id: v
11564 .get("HostedZoneId")
11565 .and_then(|x| x.as_str())
11566 .unwrap_or("")
11567 .to_string(),
11568 dns_name: v
11569 .get("DNSName")
11570 .and_then(|x| x.as_str())
11571 .unwrap_or("")
11572 .to_string(),
11573 evaluate_target_health: v
11574 .get("EvaluateTargetHealth")
11575 .and_then(|x| x.as_bool())
11576 .unwrap_or(false),
11577 });
11578 let set_identifier = props
11579 .get("SetIdentifier")
11580 .and_then(|v| v.as_str())
11581 .map(String::from);
11582 let weight = props.get("Weight").and_then(|v| {
11583 v.as_i64()
11584 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11585 });
11586 let region = props
11587 .get("Region")
11588 .and_then(|v| v.as_str())
11589 .map(String::from);
11590 let failover = props
11591 .get("Failover")
11592 .and_then(|v| v.as_str())
11593 .map(String::from);
11594 let multi_value_answer = props.get("MultiValueAnswer").and_then(|v| v.as_bool());
11595 let health_check_id = props
11596 .get("HealthCheckId")
11597 .and_then(|v| v.as_str())
11598 .map(String::from);
11599
11600 let rrset = ResourceRecordSet {
11601 name: normalized_name.clone(),
11602 record_type: record_type.clone(),
11603 set_identifier: set_identifier.clone(),
11604 weight,
11605 region,
11606 geo_location: None,
11607 failover,
11608 multi_value_answer,
11609 ttl,
11610 resource_records,
11611 alias_target,
11612 health_check_id,
11613 traffic_policy_instance_id: None,
11614 cidr_routing_config: None,
11615 geo_proximity_location: None,
11616 };
11617
11618 let mut accounts = self.route53_state.write();
11619 let state = accounts.entry("000000000000");
11622 let zone = state.hosted_zones.get_mut(&zone_id).ok_or_else(|| {
11623 format!(
11624 "HostedZone {zone_id} not yet provisioned; will retry once it has been provisioned"
11625 )
11626 })?;
11627 zone.resource_record_sets.retain(|r| {
11629 !(r.name == rrset.name
11630 && r.record_type == rrset.record_type
11631 && r.set_identifier == rrset.set_identifier)
11632 });
11633 zone.resource_record_sets.push(rrset);
11634
11635 let physical_id = match &set_identifier {
11636 Some(sid) => format!("{zone_id}|{normalized_name}|{record_type}|{sid}"),
11637 None => format!("{zone_id}|{normalized_name}|{record_type}"),
11638 };
11639 Ok(ProvisionResult::new(physical_id))
11640 }
11641
11642 fn delete_route53_record_set(
11643 &self,
11644 physical_id: &str,
11645 _attributes: &BTreeMap<String, String>,
11646 ) -> Result<(), String> {
11647 let parts: Vec<&str> = physical_id.split('|').collect();
11648 if parts.len() < 3 {
11649 return Ok(());
11650 }
11651 let zone_id = parts[0];
11652 let name = parts[1];
11653 let record_type = parts[2];
11654 let set_identifier = parts.get(3).map(|s| s.to_string());
11655
11656 let mut accounts = self.route53_state.write();
11657 let state = accounts.entry("000000000000");
11660 if let Some(zone) = state.hosted_zones.get_mut(zone_id) {
11661 zone.resource_record_sets.retain(|r| {
11662 !(r.name == name
11663 && r.record_type == record_type
11664 && r.set_identifier == set_identifier)
11665 });
11666 }
11667 Ok(())
11668 }
11669
11670 fn create_route53_health_check(
11671 &self,
11672 resource: &ResourceDefinition,
11673 ) -> Result<ProvisionResult, String> {
11674 let props = &resource.properties;
11675 let cfg_value = props
11676 .get("HealthCheckConfig")
11677 .ok_or("HealthCheckConfig is required")?;
11678
11679 let health_check_type = cfg_value
11680 .get("Type")
11681 .and_then(|v| v.as_str())
11682 .ok_or("HealthCheckConfig.Type is required")?
11683 .to_string();
11684
11685 let cfg = HealthCheckConfig {
11686 ip_address: cfg_value
11687 .get("IPAddress")
11688 .and_then(|v| v.as_str())
11689 .map(String::from),
11690 port: cfg_value
11691 .get("Port")
11692 .and_then(|v| {
11693 v.as_i64()
11694 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11695 })
11696 .map(|n| n as i32),
11697 health_check_type,
11698 resource_path: cfg_value
11699 .get("ResourcePath")
11700 .and_then(|v| v.as_str())
11701 .map(String::from),
11702 fully_qualified_domain_name: cfg_value
11703 .get("FullyQualifiedDomainName")
11704 .and_then(|v| v.as_str())
11705 .map(String::from),
11706 search_string: cfg_value
11707 .get("SearchString")
11708 .and_then(|v| v.as_str())
11709 .map(String::from),
11710 request_interval: cfg_value
11711 .get("RequestInterval")
11712 .and_then(|v| {
11713 v.as_i64()
11714 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11715 })
11716 .map(|n| n as i32),
11717 failure_threshold: cfg_value
11718 .get("FailureThreshold")
11719 .and_then(|v| {
11720 v.as_i64()
11721 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11722 })
11723 .map(|n| n as i32),
11724 measure_latency: cfg_value.get("MeasureLatency").and_then(|v| v.as_bool()),
11725 inverted: cfg_value.get("Inverted").and_then(|v| v.as_bool()),
11726 disabled: cfg_value.get("Disabled").and_then(|v| v.as_bool()),
11727 health_threshold: cfg_value
11728 .get("HealthThreshold")
11729 .and_then(|v| {
11730 v.as_i64()
11731 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11732 })
11733 .map(|n| n as i32),
11734 child_health_checks: cfg_value
11735 .get("ChildHealthChecks")
11736 .and_then(|v| v.as_array())
11737 .map(|arr| fakecloud_route53::model::ChildHealthChecks {
11738 child_health_check: arr
11739 .iter()
11740 .filter_map(|v| v.as_str().map(String::from))
11741 .collect(),
11742 }),
11743 enable_sni: cfg_value.get("EnableSNI").and_then(|v| v.as_bool()),
11744 regions: cfg_value
11745 .get("Regions")
11746 .and_then(|v| v.as_array())
11747 .map(|arr| fakecloud_route53::model::HealthCheckRegions {
11748 region: arr
11749 .iter()
11750 .filter_map(|v| v.as_str().map(String::from))
11751 .collect(),
11752 }),
11753 alarm_identifier: cfg_value.get("AlarmIdentifier").map(|v| {
11754 fakecloud_route53::model::AlarmIdentifier {
11755 region: v
11756 .get("Region")
11757 .and_then(|x| x.as_str())
11758 .unwrap_or("")
11759 .to_string(),
11760 name: v
11761 .get("Name")
11762 .and_then(|x| x.as_str())
11763 .unwrap_or("")
11764 .to_string(),
11765 }
11766 }),
11767 insufficient_data_health_status: cfg_value
11768 .get("InsufficientDataHealthStatus")
11769 .and_then(|v| v.as_str())
11770 .map(String::from),
11771 routing_control_arn: cfg_value
11772 .get("RoutingControlArn")
11773 .and_then(|v| v.as_str())
11774 .map(String::from),
11775 };
11776
11777 let id = Uuid::new_v4().to_string();
11778 let hc = StoredHealthCheck {
11779 id: id.clone(),
11780 caller_reference: format!("cfn-{}", resource.logical_id),
11781 version: 1,
11782 config: cfg,
11783 created_time: Utc::now(),
11784 status: fakecloud_route53::HealthCheckStatus::Success,
11785 last_failure_reason: None,
11786 };
11787
11788 let mut accounts = self.route53_state.write();
11789 let state = accounts.entry("000000000000");
11792 state.health_checks.insert(id.clone(), hc);
11793
11794 Ok(ProvisionResult::new(id.clone()).with("HealthCheckId", id))
11795 }
11796
11797 fn delete_route53_health_check(&self, physical_id: &str) -> Result<(), String> {
11798 let mut accounts = self.route53_state.write();
11799 let state = accounts.entry("000000000000");
11802 state.health_checks.remove(physical_id);
11803 Ok(())
11804 }
11805
11806 fn create_route53_dnssec(
11810 &self,
11811 resource: &ResourceDefinition,
11812 ) -> Result<ProvisionResult, String> {
11813 let zone_id = resource
11814 .properties
11815 .get("HostedZoneId")
11816 .and_then(|v| v.as_str())
11817 .ok_or_else(|| "HostedZoneId is required".to_string())?
11818 .trim_start_matches("/hostedzone/")
11819 .to_string();
11820 let mut accounts = self.route53_state.write();
11821 let state = accounts.entry("000000000000");
11822 if !state.hosted_zones.contains_key(&zone_id) {
11823 return Err(format!("HostedZone {zone_id} does not exist"));
11824 }
11825 state
11826 .dnssec_status
11827 .insert(zone_id.clone(), "SIGNING".to_string());
11828 Ok(ProvisionResult::new(zone_id))
11829 }
11830
11831 fn delete_route53_dnssec(&self, physical_id: &str) -> Result<(), String> {
11832 let mut accounts = self.route53_state.write();
11833 let state = accounts.entry("000000000000");
11834 state.dnssec_status.remove(physical_id);
11835 Ok(())
11836 }
11837
11838 fn create_route53_key_signing_key(
11842 &self,
11843 resource: &ResourceDefinition,
11844 ) -> Result<ProvisionResult, String> {
11845 let props = &resource.properties;
11846 let zone_id = props
11847 .get("HostedZoneId")
11848 .and_then(|v| v.as_str())
11849 .ok_or_else(|| "HostedZoneId is required".to_string())?
11850 .trim_start_matches("/hostedzone/")
11851 .to_string();
11852 let name = props
11853 .get("Name")
11854 .and_then(|v| v.as_str())
11855 .ok_or_else(|| "Name is required".to_string())?
11856 .to_string();
11857 let kms_arn = props
11858 .get("KeyManagementServiceArn")
11859 .and_then(|v| v.as_str())
11860 .ok_or_else(|| "KeyManagementServiceArn is required".to_string())?
11861 .to_string();
11862 let status = props
11863 .get("Status")
11864 .and_then(|v| v.as_str())
11865 .unwrap_or("ACTIVE")
11866 .to_string();
11867 let now = Utc::now();
11868 let key_material = fakecloud_route53::dnssec::derive_keypair(&zone_id, &name);
11874 let key_tag = fakecloud_route53::dnssec::key_tag_for(&key_material.dnskey_public_key);
11875 let mut accounts = self.route53_state.write();
11876 let state = accounts.entry("000000000000");
11877 if !state.hosted_zones.contains_key(&zone_id) {
11878 return Err(format!("HostedZone {zone_id} does not exist"));
11879 }
11880 let zone_name = state
11881 .hosted_zones
11882 .get(&zone_id)
11883 .map(|z| z.name.clone())
11884 .unwrap_or_else(|| ".".to_string());
11885 let ds_digest_hex = fakecloud_route53::dnssec::ds_digest_sha256(
11886 &zone_name,
11887 key_tag,
11888 &key_material.dnskey_public_key,
11889 );
11890 let ksk = fakecloud_route53::StoredKeySigningKey {
11891 hosted_zone_id: zone_id.clone(),
11892 name: name.clone(),
11893 kms_arn,
11894 status,
11895 caller_reference: format!("cfn-{}", Uuid::new_v4()),
11896 created_date: now,
11897 last_modified_date: now,
11898 key_tag: key_tag as i32,
11899 private_key_pem: key_material.private_key_pem,
11900 public_key_der: key_material.public_key_der,
11901 ds_digest_hex,
11902 };
11903 state
11904 .key_signing_keys
11905 .insert((zone_id.clone(), name.clone()), ksk);
11906 Ok(ProvisionResult::new(format!("{zone_id}/{name}")))
11907 }
11908
11909 fn delete_route53_key_signing_key(&self, physical_id: &str) -> Result<(), String> {
11910 let (zone_id, name) = match physical_id.split_once('/') {
11911 Some(parts) => parts,
11912 None => return Ok(()),
11913 };
11914 let mut accounts = self.route53_state.write();
11915 let state = accounts.entry("000000000000");
11916 state
11917 .key_signing_keys
11918 .remove(&(zone_id.to_string(), name.to_string()));
11919 Ok(())
11920 }
11921
11922 fn create_cf_origin_access_identity(
11929 &self,
11930 resource: &ResourceDefinition,
11931 ) -> Result<ProvisionResult, String> {
11932 let props = &resource.properties;
11933 let cfg = props
11934 .get("CloudFrontOriginAccessIdentityConfig")
11935 .ok_or("CloudFrontOriginAccessIdentityConfig is required")?;
11936 let comment = cfg
11937 .get("Comment")
11938 .and_then(|v| v.as_str())
11939 .unwrap_or("")
11940 .to_string();
11941 let caller_reference = format!("cfn-{}", resource.logical_id);
11942
11943 let id = format!(
11944 "E{}",
11945 Uuid::new_v4().simple().to_string()[..13].to_uppercase()
11946 );
11947 let etag = format!(
11948 "E{}",
11949 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
11950 );
11951 let s3_canonical_user_id = format!(
11952 "{:0<64}",
11953 Uuid::new_v4().simple().to_string().to_lowercase()
11954 );
11955
11956 let oai = StoredOriginAccessIdentity {
11957 id: id.clone(),
11958 etag,
11959 s3_canonical_user_id: s3_canonical_user_id.clone(),
11960 config: CloudFrontOriginAccessIdentityConfig {
11961 caller_reference,
11962 comment,
11963 },
11964 };
11965
11966 let mut accounts = self.cloudfront_state.write();
11967 let state = accounts.entry("000000000000");
11968 state.origin_access_identities.insert(id.clone(), oai);
11969
11970 Ok(ProvisionResult::new(id.clone())
11971 .with("Id", id)
11972 .with("S3CanonicalUserId", s3_canonical_user_id))
11973 }
11974
11975 fn delete_cf_origin_access_identity(&self, physical_id: &str) -> Result<(), String> {
11976 let mut accounts = self.cloudfront_state.write();
11977 let state = accounts.entry("000000000000");
11978 state.origin_access_identities.remove(physical_id);
11979 Ok(())
11980 }
11981
11982 fn create_cf_distribution(
11988 &self,
11989 resource: &ResourceDefinition,
11990 ) -> Result<ProvisionResult, String> {
11991 let cfg = resource
11992 .properties
11993 .get("DistributionConfig")
11994 .ok_or_else(|| "DistributionConfig is required".to_string())?;
11995
11996 let origin_entries: Vec<Origin> = cfg
12003 .get("Origins")
12004 .and_then(|v| v.as_array())
12005 .ok_or_else(|| "DistributionConfig.Origins is required".to_string())?
12006 .iter()
12007 .map(|o| {
12008 let mut patched = o.clone();
12009 if let Some(custom) = patched
12010 .get_mut("CustomOriginConfig")
12011 .and_then(|v| v.as_object_mut())
12012 {
12013 if let Some(v) = custom.remove("HTTPPort") {
12014 custom.insert("HttpPort".to_string(), v);
12015 }
12016 if let Some(v) = custom.remove("HTTPSPort") {
12017 custom.insert("HttpsPort".to_string(), v);
12018 }
12019 }
12020 serde_json::from_value::<Origin>(patched)
12021 .map_err(|e| format!("Invalid Origin entry: {e}"))
12022 })
12023 .collect::<Result<Vec<_>, _>>()?;
12024 if origin_entries.is_empty() {
12025 return Err("DistributionConfig.Origins must contain at least one origin".to_string());
12026 }
12027 let origins = Origins {
12028 quantity: origin_entries.len() as i32,
12029 items: Some(OriginItems {
12030 origin: origin_entries,
12031 }),
12032 };
12033
12034 let dcb_value = cfg
12035 .get("DefaultCacheBehavior")
12036 .ok_or_else(|| "DistributionConfig.DefaultCacheBehavior is required".to_string())?;
12037 let default_cache_behavior: DefaultCacheBehavior =
12038 serde_json::from_value(dcb_value.clone())
12039 .map_err(|e| format!("Invalid DefaultCacheBehavior: {e}"))?;
12040
12041 let comment = cfg
12042 .get("Comment")
12043 .and_then(|v| v.as_str())
12044 .unwrap_or("")
12045 .to_string();
12046 let enabled = cfg.get("Enabled").and_then(|v| v.as_bool()).unwrap_or(true);
12047 let price_class = cfg
12048 .get("PriceClass")
12049 .and_then(|v| v.as_str())
12050 .map(|s| s.to_string());
12051 let http_version = cfg
12052 .get("HttpVersion")
12053 .and_then(|v| v.as_str())
12054 .map(|s| s.to_string());
12055 let is_ipv6_enabled = cfg.get("IPV6Enabled").and_then(|v| v.as_bool());
12056 let default_root_object = cfg
12057 .get("DefaultRootObject")
12058 .and_then(|v| v.as_str())
12059 .map(|s| s.to_string());
12060 let web_acl_id = cfg
12061 .get("WebACLId")
12062 .and_then(|v| v.as_str())
12063 .map(|s| s.to_string());
12064
12065 let viewer_certificate: Option<ViewerCertificate> = cfg
12066 .get("ViewerCertificate")
12067 .map(|v| serde_json::from_value(v.clone()))
12068 .transpose()
12069 .map_err(|e| format!("Invalid ViewerCertificate: {e}"))?;
12070
12071 let caller_reference = format!("cfn-{}-{}", resource.logical_id, Uuid::new_v4().simple());
12072
12073 let mut config = DistributionConfig {
12074 caller_reference,
12075 comment,
12076 enabled,
12077 origins,
12078 default_cache_behavior,
12079 ..Default::default()
12080 };
12081 config.price_class = price_class;
12082 config.http_version = http_version;
12083 config.is_ipv6_enabled = is_ipv6_enabled;
12084 config.default_root_object = default_root_object;
12085 config.web_acl_id = web_acl_id;
12086 config.viewer_certificate = viewer_certificate;
12087
12088 let id_suffix: String = Uuid::new_v4()
12091 .simple()
12092 .to_string()
12093 .chars()
12094 .take(13)
12095 .collect::<String>()
12096 .to_uppercase();
12097 let id = format!("E{id_suffix}");
12098 let etag_suffix: String = Uuid::new_v4()
12099 .simple()
12100 .to_string()
12101 .chars()
12102 .take(7)
12103 .collect::<String>()
12104 .to_uppercase();
12105 let etag = format!("E{etag_suffix}");
12106 let domain_name = format!("{}.cloudfront.net", id.to_lowercase());
12107 let arn = format!(
12108 "arn:aws:cloudfront::{}:distribution/{}",
12109 self.account_id, id
12110 );
12111
12112 let stored = StoredDistribution {
12113 id: id.clone(),
12114 arn: arn.clone(),
12115 status: "InProgress".to_string(),
12118 last_modified_time: Utc::now(),
12119 domain_name: domain_name.clone(),
12120 in_progress_invalidation_batches: 0,
12121 etag,
12122 config,
12123 };
12124
12125 let mut accounts = self.cloudfront_state.write();
12126 let state = accounts.entry("000000000000");
12127 state.distributions.insert(id.clone(), stored);
12128 Ok(ProvisionResult::new(id.clone())
12129 .with("Id", id)
12130 .with("DomainName", domain_name)
12131 .with("Arn", arn))
12132 }
12133
12134 fn delete_cf_distribution(&self, physical_id: &str) -> Result<(), String> {
12135 let mut accounts = self.cloudfront_state.write();
12136 let state = accounts.entry("000000000000");
12137 state.distributions.remove(physical_id);
12138 Ok(())
12139 }
12140
12141 fn create_cf_origin_access_control(
12142 &self,
12143 resource: &ResourceDefinition,
12144 ) -> Result<ProvisionResult, String> {
12145 let props = &resource.properties;
12146 let cfg = props
12147 .get("OriginAccessControlConfig")
12148 .ok_or("OriginAccessControlConfig is required")?;
12149 let name = cfg
12150 .get("Name")
12151 .and_then(|v| v.as_str())
12152 .ok_or("OriginAccessControlConfig.Name is required")?
12153 .to_string();
12154 let signing_protocol = cfg
12155 .get("SigningProtocol")
12156 .and_then(|v| v.as_str())
12157 .unwrap_or("sigv4")
12158 .to_string();
12159 let signing_behavior = cfg
12160 .get("SigningBehavior")
12161 .and_then(|v| v.as_str())
12162 .unwrap_or("always")
12163 .to_string();
12164 let origin_type = cfg
12165 .get("OriginAccessControlOriginType")
12166 .and_then(|v| v.as_str())
12167 .ok_or("OriginAccessControlConfig.OriginAccessControlOriginType is required")?
12168 .to_string();
12169 let description = cfg
12170 .get("Description")
12171 .and_then(|v| v.as_str())
12172 .map(String::from);
12173
12174 let id = format!(
12175 "E{}",
12176 Uuid::new_v4().simple().to_string()[..13].to_uppercase()
12177 );
12178 let etag = format!(
12179 "E{}",
12180 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12181 );
12182 let oac = StoredOriginAccessControl {
12183 id: id.clone(),
12184 etag,
12185 config: OriginAccessControlConfig {
12186 name,
12187 description,
12188 signing_protocol,
12189 signing_behavior,
12190 origin_access_control_origin_type: origin_type,
12191 },
12192 };
12193
12194 let mut accounts = self.cloudfront_state.write();
12195 let state = accounts.entry("000000000000");
12196 state.origin_access_controls.insert(id.clone(), oac);
12197
12198 Ok(ProvisionResult::new(id.clone()).with("Id", id))
12199 }
12200
12201 fn delete_cf_origin_access_control(&self, physical_id: &str) -> Result<(), String> {
12202 let mut accounts = self.cloudfront_state.write();
12203 let state = accounts.entry("000000000000");
12204 state.origin_access_controls.remove(physical_id);
12205 Ok(())
12206 }
12207
12208 fn create_cf_public_key(
12209 &self,
12210 resource: &ResourceDefinition,
12211 ) -> Result<ProvisionResult, String> {
12212 let props = &resource.properties;
12213 let cfg = props
12214 .get("PublicKeyConfig")
12215 .ok_or("PublicKeyConfig is required")?;
12216 let name = cfg
12217 .get("Name")
12218 .and_then(|v| v.as_str())
12219 .ok_or("PublicKeyConfig.Name is required")?
12220 .to_string();
12221 let encoded_key = cfg
12222 .get("EncodedKey")
12223 .and_then(|v| v.as_str())
12224 .ok_or("PublicKeyConfig.EncodedKey is required")?
12225 .to_string();
12226 let comment = cfg
12227 .get("Comment")
12228 .and_then(|v| v.as_str())
12229 .map(String::from);
12230 let caller_reference = cfg
12231 .get("CallerReference")
12232 .and_then(|v| v.as_str())
12233 .unwrap_or("")
12234 .to_string();
12235 let caller_reference = if caller_reference.is_empty() {
12236 format!("cfn-{}", resource.logical_id)
12237 } else {
12238 caller_reference
12239 };
12240
12241 let id = format!(
12242 "K{}",
12243 Uuid::new_v4().simple().to_string()[..13].to_uppercase()
12244 );
12245 let etag = format!(
12246 "E{}",
12247 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12248 );
12249
12250 let pk = StoredPublicKey {
12251 id: id.clone(),
12252 etag,
12253 created_time: Utc::now(),
12254 config: PublicKeyConfig {
12255 caller_reference,
12256 name,
12257 encoded_key,
12258 comment,
12259 },
12260 };
12261
12262 let mut accounts = self.cloudfront_state.write();
12263 let state = accounts.entry("000000000000");
12264 state.public_keys.insert(id.clone(), pk);
12265
12266 Ok(ProvisionResult::new(id.clone()).with("Id", id))
12267 }
12268
12269 fn delete_cf_public_key(&self, physical_id: &str) -> Result<(), String> {
12270 let mut accounts = self.cloudfront_state.write();
12271 let state = accounts.entry("000000000000");
12272 state.public_keys.remove(physical_id);
12273 Ok(())
12274 }
12275
12276 fn create_cf_key_group(
12277 &self,
12278 resource: &ResourceDefinition,
12279 ) -> Result<ProvisionResult, String> {
12280 let props = &resource.properties;
12281 let cfg = props
12282 .get("KeyGroupConfig")
12283 .ok_or("KeyGroupConfig is required")?;
12284 let name = cfg
12285 .get("Name")
12286 .and_then(|v| v.as_str())
12287 .ok_or("KeyGroupConfig.Name is required")?
12288 .to_string();
12289 let items: Vec<String> = cfg
12290 .get("Items")
12291 .and_then(|v| v.as_array())
12292 .map(|arr| {
12293 arr.iter()
12294 .filter_map(|v| v.as_str().map(String::from))
12295 .collect()
12296 })
12297 .unwrap_or_default();
12298 let comment = cfg
12299 .get("Comment")
12300 .and_then(|v| v.as_str())
12301 .map(String::from);
12302
12303 let id = format!(
12304 "KG{}",
12305 Uuid::new_v4().simple().to_string()[..12].to_uppercase()
12306 );
12307 let etag = format!(
12308 "E{}",
12309 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12310 );
12311
12312 let kg = StoredKeyGroup {
12313 id: id.clone(),
12314 etag,
12315 last_modified_time: Utc::now(),
12316 config: KeyGroupConfig {
12317 name,
12318 items: KeyGroupItems { public_key: items },
12319 comment,
12320 },
12321 };
12322
12323 let mut accounts = self.cloudfront_state.write();
12324 let state = accounts.entry("000000000000");
12325 state.key_groups.insert(id.clone(), kg);
12326
12327 Ok(ProvisionResult::new(id.clone()).with("Id", id))
12328 }
12329
12330 fn delete_cf_key_group(&self, physical_id: &str) -> Result<(), String> {
12331 let mut accounts = self.cloudfront_state.write();
12332 let state = accounts.entry("000000000000");
12333 state.key_groups.remove(physical_id);
12334 Ok(())
12335 }
12336
12337 fn create_cf_function(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
12338 let props = &resource.properties;
12339 let name = props
12340 .get("Name")
12341 .and_then(|v| v.as_str())
12342 .ok_or("Name is required")?
12343 .to_string();
12344 let function_code = props
12345 .get("FunctionCode")
12346 .and_then(|v| v.as_str())
12347 .ok_or("FunctionCode is required")?
12348 .to_string();
12349 let cfg = props
12350 .get("FunctionConfig")
12351 .ok_or("FunctionConfig is required")?;
12352 let runtime = cfg
12353 .get("Runtime")
12354 .and_then(|v| v.as_str())
12355 .unwrap_or("cloudfront-js-2.0")
12356 .to_string();
12357 let comment = cfg
12358 .get("Comment")
12359 .and_then(|v| v.as_str())
12360 .map(String::from);
12361
12362 let id = format!(
12363 "FN{}",
12364 Uuid::new_v4().simple().to_string()[..12].to_uppercase()
12365 );
12366 let etag = format!(
12367 "E{}",
12368 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12369 );
12370 let function_arn = format!("arn:aws:cloudfront::{}:function/{}", self.account_id, name);
12371
12372 let now = Utc::now();
12373 let func = StoredFunction {
12374 name: name.clone(),
12375 etag,
12376 status: "UNPUBLISHED".to_string(),
12377 stage: "DEVELOPMENT".to_string(),
12378 function_arn: function_arn.clone(),
12379 created_time: now,
12380 last_modified_time: now,
12381 config: FunctionConfig {
12382 comment,
12383 runtime,
12384 key_value_store_associations: None,
12385 },
12386 function_code,
12387 live_function_code: None,
12388 };
12389
12390 let mut accounts = self.cloudfront_state.write();
12391 let state = accounts.entry("000000000000");
12392 state.functions.insert(name.clone(), func);
12395
12396 Ok(ProvisionResult::new(name.clone())
12397 .with("FunctionARN", function_arn)
12398 .with("FunctionMetadata.FunctionARN", id)
12399 .with("Stage", "DEVELOPMENT"))
12400 }
12401
12402 fn delete_cf_function(&self, physical_id: &str) -> Result<(), String> {
12403 let mut accounts = self.cloudfront_state.write();
12404 let state = accounts.entry("000000000000");
12405 state.functions.remove(physical_id);
12406 Ok(())
12407 }
12408
12409 fn create_cf_cache_policy(
12410 &self,
12411 resource: &ResourceDefinition,
12412 ) -> Result<ProvisionResult, String> {
12413 let props = &resource.properties;
12414 let cfg = props
12415 .get("CachePolicyConfig")
12416 .ok_or("CachePolicyConfig is required")?;
12417 let name = cfg
12418 .get("Name")
12419 .and_then(|v| v.as_str())
12420 .ok_or("CachePolicyConfig.Name is required")?
12421 .to_string();
12422 let min_ttl = cfg
12423 .get("MinTTL")
12424 .and_then(|v| {
12425 v.as_i64()
12426 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
12427 })
12428 .unwrap_or(0);
12429 let default_ttl = cfg.get("DefaultTTL").and_then(|v| {
12430 v.as_i64()
12431 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
12432 });
12433 let max_ttl = cfg.get("MaxTTL").and_then(|v| {
12434 v.as_i64()
12435 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
12436 });
12437 let comment = cfg
12438 .get("Comment")
12439 .and_then(|v| v.as_str())
12440 .map(String::from);
12441
12442 let id = format!(
12443 "CP{}",
12444 Uuid::new_v4().simple().to_string()[..12].to_uppercase()
12445 );
12446 let etag = format!(
12447 "E{}",
12448 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12449 );
12450
12451 let cache_policy = StoredCachePolicy {
12452 id: id.clone(),
12453 etag,
12454 last_modified_time: Utc::now(),
12455 config: CachePolicyConfig {
12456 comment,
12457 name,
12458 default_ttl,
12459 max_ttl,
12460 min_ttl,
12461 parameters_in_cache_key_and_forwarded_to_origin: None,
12462 },
12463 policy_type: "custom".to_string(),
12464 };
12465
12466 let mut accounts = self.cloudfront_state.write();
12467 let state = accounts.entry("000000000000");
12468 state.cache_policies.insert(id.clone(), cache_policy);
12469
12470 Ok(ProvisionResult::new(id.clone()).with("Id", id))
12471 }
12472
12473 fn delete_cf_cache_policy(&self, physical_id: &str) -> Result<(), String> {
12474 let mut accounts = self.cloudfront_state.write();
12475 let state = accounts.entry("000000000000");
12476 state.cache_policies.remove(physical_id);
12477 Ok(())
12478 }
12479
12480 fn create_cf_origin_request_policy(
12481 &self,
12482 resource: &ResourceDefinition,
12483 ) -> Result<ProvisionResult, String> {
12484 let props = &resource.properties;
12485 let cfg = props
12486 .get("OriginRequestPolicyConfig")
12487 .ok_or("OriginRequestPolicyConfig is required")?;
12488 let name = cfg
12489 .get("Name")
12490 .and_then(|v| v.as_str())
12491 .ok_or("OriginRequestPolicyConfig.Name is required")?
12492 .to_string();
12493 let header_behavior = cfg
12494 .get("HeadersConfig")
12495 .and_then(|v| v.get("HeaderBehavior"))
12496 .and_then(|v| v.as_str())
12497 .unwrap_or("none")
12498 .to_string();
12499 let cookie_behavior = cfg
12500 .get("CookiesConfig")
12501 .and_then(|v| v.get("CookieBehavior"))
12502 .and_then(|v| v.as_str())
12503 .unwrap_or("none")
12504 .to_string();
12505 let query_string_behavior = cfg
12506 .get("QueryStringsConfig")
12507 .and_then(|v| v.get("QueryStringBehavior"))
12508 .and_then(|v| v.as_str())
12509 .unwrap_or("none")
12510 .to_string();
12511 let comment = cfg
12512 .get("Comment")
12513 .and_then(|v| v.as_str())
12514 .map(String::from);
12515
12516 let id = format!(
12517 "ORP{}",
12518 Uuid::new_v4().simple().to_string()[..11].to_uppercase()
12519 );
12520 let etag = format!(
12521 "E{}",
12522 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12523 );
12524
12525 let policy = StoredOriginRequestPolicy {
12526 id: id.clone(),
12527 etag,
12528 last_modified_time: Utc::now(),
12529 config: OriginRequestPolicyConfig {
12530 comment,
12531 name,
12532 headers_config: OriginRequestPolicyHeadersConfig {
12533 header_behavior,
12534 headers: None,
12535 },
12536 cookies_config: OriginRequestPolicyCookiesConfig {
12537 cookie_behavior,
12538 cookies: None,
12539 },
12540 query_strings_config: OriginRequestPolicyQueryStringsConfig {
12541 query_string_behavior,
12542 query_strings: None,
12543 },
12544 },
12545 policy_type: "custom".to_string(),
12546 };
12547
12548 let mut accounts = self.cloudfront_state.write();
12549 let state = accounts.entry("000000000000");
12550 state.origin_request_policies.insert(id.clone(), policy);
12551
12552 Ok(ProvisionResult::new(id.clone()).with("Id", id))
12553 }
12554
12555 fn delete_cf_origin_request_policy(&self, physical_id: &str) -> Result<(), String> {
12556 let mut accounts = self.cloudfront_state.write();
12557 let state = accounts.entry("000000000000");
12558 state.origin_request_policies.remove(physical_id);
12559 Ok(())
12560 }
12561
12562 fn create_cf_response_headers_policy(
12563 &self,
12564 resource: &ResourceDefinition,
12565 ) -> Result<ProvisionResult, String> {
12566 let props = &resource.properties;
12567 let cfg = props
12568 .get("ResponseHeadersPolicyConfig")
12569 .ok_or("ResponseHeadersPolicyConfig is required")?;
12570 let name = cfg
12571 .get("Name")
12572 .and_then(|v| v.as_str())
12573 .ok_or("ResponseHeadersPolicyConfig.Name is required")?
12574 .to_string();
12575 let comment = cfg
12576 .get("Comment")
12577 .and_then(|v| v.as_str())
12578 .map(String::from);
12579
12580 let id = format!(
12581 "RHP{}",
12582 Uuid::new_v4().simple().to_string()[..11].to_uppercase()
12583 );
12584 let etag = format!(
12585 "E{}",
12586 Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12587 );
12588
12589 let policy = StoredResponseHeadersPolicy {
12590 id: id.clone(),
12591 etag,
12592 last_modified_time: Utc::now(),
12593 config: ResponseHeadersPolicyConfig {
12594 comment,
12595 name,
12596 cors_config: None,
12597 security_headers_config: None,
12598 server_timing_headers_config: None,
12599 custom_headers_config: None,
12600 remove_headers_config: None,
12601 },
12602 policy_type: "custom".to_string(),
12603 };
12604
12605 let mut accounts = self.cloudfront_state.write();
12606 let state = accounts.entry("000000000000");
12607 state.response_headers_policies.insert(id.clone(), policy);
12608
12609 Ok(ProvisionResult::new(id.clone()).with("Id", id))
12610 }
12611
12612 fn delete_cf_response_headers_policy(&self, physical_id: &str) -> Result<(), String> {
12613 let mut accounts = self.cloudfront_state.write();
12614 let state = accounts.entry("000000000000");
12615 state.response_headers_policies.remove(physical_id);
12616 Ok(())
12617 }
12618
12619 fn create_sfn_state_machine(
12622 &self,
12623 resource: &ResourceDefinition,
12624 ) -> Result<ProvisionResult, String> {
12625 let props = &resource.properties;
12626 let name = props
12627 .get("StateMachineName")
12628 .and_then(|v| v.as_str())
12629 .map(String::from)
12630 .unwrap_or_else(|| {
12631 let suffix = Uuid::new_v4().simple().to_string();
12632 format!("{}-{}", resource.logical_id, &suffix[..8])
12633 });
12634 let role_arn = props
12635 .get("RoleArn")
12636 .and_then(|v| v.as_str())
12637 .ok_or("RoleArn is required")?
12638 .to_string();
12639 let machine_type_str = props
12640 .get("StateMachineType")
12641 .and_then(|v| v.as_str())
12642 .unwrap_or("STANDARD");
12643 let machine_type = StateMachineType::parse(machine_type_str)
12644 .ok_or_else(|| format!("Invalid StateMachineType: {machine_type_str}"))?;
12645 let definition = props
12646 .get("DefinitionString")
12647 .and_then(|v| v.as_str())
12648 .map(String::from)
12649 .or_else(|| {
12650 props
12651 .get("Definition")
12652 .map(|v| serde_json::to_string(v).unwrap_or_default())
12653 })
12654 .ok_or("Definition or DefinitionString is required")?;
12655 let logging_configuration = props.get("LoggingConfiguration").cloned();
12656 let tracing_configuration = props.get("TracingConfiguration").cloned();
12657
12658 let arn = format!(
12659 "arn:aws:states:{}:{}:stateMachine:{}",
12660 self.region, self.account_id, name
12661 );
12662 let now = Utc::now();
12663 let revision_id = Uuid::new_v4().to_string();
12664
12665 let sm = StateMachine {
12666 name: name.clone(),
12667 arn: arn.clone(),
12668 definition,
12669 role_arn,
12670 machine_type,
12671 status: StateMachineStatus::Active,
12672 creation_date: now,
12673 update_date: now,
12674 tags: BTreeMap::new(),
12675 revision_id,
12676 logging_configuration,
12677 tracing_configuration,
12678 description: String::new(),
12679 };
12680
12681 let mut accounts = self.stepfunctions_state.write();
12682 let state = accounts.get_or_create(&self.account_id);
12683 state.state_machines.insert(arn.clone(), sm);
12684
12685 Ok(ProvisionResult::new(arn.clone())
12686 .with("Arn", arn.clone())
12687 .with("Name", name)
12688 .with("StateMachineRevisionId", "INITIAL"))
12689 }
12690
12691 fn delete_sfn_state_machine(&self, physical_id: &str) -> Result<(), String> {
12692 let mut accounts = self.stepfunctions_state.write();
12693 let state = accounts.get_or_create(&self.account_id);
12694 state.state_machines.remove(physical_id);
12695 Ok(())
12696 }
12697
12698 fn create_sfn_activity(
12699 &self,
12700 resource: &ResourceDefinition,
12701 ) -> Result<ProvisionResult, String> {
12702 let props = &resource.properties;
12703 let name = props
12704 .get("Name")
12705 .and_then(|v| v.as_str())
12706 .ok_or("Name is required")?
12707 .to_string();
12708 let arn = format!(
12709 "arn:aws:states:{}:{}:activity:{}",
12710 self.region, self.account_id, name
12711 );
12712 let activity = SfnActivity {
12713 name: name.clone(),
12714 arn: arn.clone(),
12715 creation_date: Utc::now(),
12716 tags: BTreeMap::new(),
12717 };
12718
12719 let mut accounts = self.stepfunctions_state.write();
12720 let state = accounts.get_or_create(&self.account_id);
12721 state.activities.insert(arn.clone(), activity);
12722
12723 Ok(ProvisionResult::new(arn.clone())
12724 .with("Arn", arn)
12725 .with("Name", name))
12726 }
12727
12728 fn delete_sfn_activity(&self, physical_id: &str) -> Result<(), String> {
12729 let mut accounts = self.stepfunctions_state.write();
12730 let state = accounts.get_or_create(&self.account_id);
12731 state.activities.remove(physical_id);
12732 Ok(())
12733 }
12734
12735 fn create_sfn_version(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
12736 let props = &resource.properties;
12737 let sm_arn = props
12738 .get("StateMachineArn")
12739 .and_then(|v| v.as_str())
12740 .ok_or("StateMachineArn is required")?
12741 .to_string();
12742 let description = props
12743 .get("Description")
12744 .and_then(|v| v.as_str())
12745 .unwrap_or("")
12746 .to_string();
12747 let revision_id = props
12748 .get("StateMachineRevisionId")
12749 .and_then(|v| v.as_str())
12750 .unwrap_or("INITIAL")
12751 .to_string();
12752
12753 let mut accounts = self.stepfunctions_state.write();
12754 let state = accounts.get_or_create(&self.account_id);
12755
12756 let next_version = state
12758 .state_machine_versions
12759 .values()
12760 .filter(|v| v.state_machine_arn == sm_arn)
12761 .map(|v| v.version)
12762 .max()
12763 .unwrap_or(0)
12764 + 1;
12765 let version_arn = format!("{sm_arn}:{next_version}");
12766
12767 let version = StateMachineVersion {
12768 state_machine_arn: sm_arn,
12769 version: next_version,
12770 revision_id,
12771 description,
12772 creation_date: Utc::now(),
12773 };
12774 state
12775 .state_machine_versions
12776 .insert(version_arn.clone(), version);
12777
12778 Ok(ProvisionResult::new(version_arn.clone()).with("Arn", version_arn))
12779 }
12780
12781 fn delete_sfn_version(&self, physical_id: &str) -> Result<(), String> {
12782 let mut accounts = self.stepfunctions_state.write();
12783 let state = accounts.get_or_create(&self.account_id);
12784 state.state_machine_versions.remove(physical_id);
12785 Ok(())
12786 }
12787
12788 fn create_sfn_alias(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
12789 let props = &resource.properties;
12790 let name = props
12791 .get("Name")
12792 .and_then(|v| v.as_str())
12793 .ok_or("Name is required")?
12794 .to_string();
12795 let description = props
12796 .get("Description")
12797 .and_then(|v| v.as_str())
12798 .unwrap_or("")
12799 .to_string();
12800 let routes_value = props
12801 .get("RoutingConfiguration")
12802 .and_then(|v| v.as_array())
12803 .ok_or("RoutingConfiguration is required")?;
12804 let routing_configuration: Vec<AliasRoute> = routes_value
12805 .iter()
12806 .map(|r| AliasRoute {
12807 state_machine_version_arn: r
12808 .get("StateMachineVersionArn")
12809 .and_then(|x| x.as_str())
12810 .unwrap_or("")
12811 .to_string(),
12812 weight: r
12813 .get("Weight")
12814 .and_then(|x| {
12815 x.as_i64()
12816 .or_else(|| x.as_str().and_then(|s| s.parse::<i64>().ok()))
12817 })
12818 .map(|w| w as i32)
12819 .unwrap_or(0),
12820 })
12821 .collect();
12822
12823 let first_version_arn = routing_configuration
12824 .first()
12825 .map(|r| r.state_machine_version_arn.clone())
12826 .unwrap_or_default();
12827 let sm_arn_root = first_version_arn
12830 .rsplit_once(':')
12831 .map(|(root, _)| root.to_string())
12832 .unwrap_or_else(|| {
12833 format!(
12834 "arn:aws:states:{}:{}:stateMachine:unknown",
12835 self.region, self.account_id
12836 )
12837 });
12838 let arn = format!("{sm_arn_root}:{name}");
12839 let now = Utc::now();
12840 let alias = StateMachineAlias {
12841 name: name.clone(),
12842 arn: arn.clone(),
12843 description,
12844 routing_configuration,
12845 creation_date: now,
12846 update_date: now,
12847 };
12848
12849 let mut accounts = self.stepfunctions_state.write();
12850 let state = accounts.get_or_create(&self.account_id);
12851 state.state_machine_aliases.insert(arn.clone(), alias);
12852
12853 Ok(ProvisionResult::new(arn.clone())
12854 .with("Arn", arn)
12855 .with("Name", name))
12856 }
12857
12858 fn delete_sfn_alias(&self, physical_id: &str) -> Result<(), String> {
12859 let mut accounts = self.stepfunctions_state.write();
12860 let state = accounts.get_or_create(&self.account_id);
12861 state.state_machine_aliases.remove(physical_id);
12862 Ok(())
12863 }
12864
12865 fn create_wafv2_web_acl(
12873 &self,
12874 resource: &ResourceDefinition,
12875 ) -> Result<ProvisionResult, String> {
12876 let props = &resource.properties;
12877 let name = props
12878 .get("Name")
12879 .and_then(|v| v.as_str())
12880 .ok_or("Name is required")?
12881 .to_string();
12882 let scope = props
12883 .get("Scope")
12884 .and_then(|v| v.as_str())
12885 .ok_or("Scope is required")?
12886 .to_string();
12887 let default_action = props
12888 .get("DefaultAction")
12889 .cloned()
12890 .unwrap_or_else(|| serde_json::json!({"Allow": {}}));
12891 let description = props
12892 .get("Description")
12893 .and_then(|v| v.as_str())
12894 .map(String::from);
12895 let rules = props
12896 .get("Rules")
12897 .and_then(|v| v.as_array())
12898 .cloned()
12899 .unwrap_or_default();
12900 let visibility_config = props
12901 .get("VisibilityConfig")
12902 .cloned()
12903 .unwrap_or_else(|| serde_json::json!({}));
12904 let capacity = props.get("Capacity").and_then(|v| v.as_i64()).unwrap_or(0);
12905
12906 let id = Uuid::new_v4().to_string();
12907 let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
12908 ("us-east-1", "global".to_string())
12909 } else {
12910 (self.region.as_str(), self.region.clone())
12911 };
12912 let arn = format!(
12913 "arn:aws:wafv2:{}:{}:{}/webacl/{}/{}",
12914 region_in_arn, self.account_id, scope_seg, name, id
12915 );
12916 let acl = WebAcl {
12917 id: id.clone(),
12918 name: name.clone(),
12919 arn: arn.clone(),
12920 scope: scope.clone(),
12921 default_action,
12922 description,
12923 rules,
12924 visibility_config,
12925 capacity,
12926 lock_token: Uuid::new_v4().simple().to_string(),
12927 label_namespace: format!("awswaf:{}:webacl:{}:", self.account_id, name),
12928 custom_response_bodies: BTreeMap::new(),
12929 captcha_config: None,
12930 challenge_config: None,
12931 token_domains: Vec::new(),
12932 association_config: None,
12933 data_protection_config: None,
12934 on_source_d_do_s_protection_config: None,
12935 application_config: None,
12936 retrofitted_by_firewall_manager: false,
12937 pre_process_firewall_manager_rule_groups: Vec::new(),
12938 post_process_firewall_manager_rule_groups: Vec::new(),
12939 managed_by_firewall_manager: false,
12940 created_time: Utc::now(),
12941 };
12942
12943 let mut accounts = self.wafv2_state.write();
12944 let state = accounts
12945 .accounts
12946 .entry(self.account_id.clone())
12947 .or_default();
12948 state.web_acls.insert((scope.clone(), name.clone()), acl);
12949
12950 Ok(ProvisionResult::new(arn.clone())
12951 .with("Arn", arn)
12952 .with("Id", id)
12953 .with("Name", name)
12954 .with("Capacity", capacity.to_string()))
12955 }
12956
12957 fn delete_wafv2_web_acl(&self, physical_id: &str) -> Result<(), String> {
12958 let mut accounts = self.wafv2_state.write();
12959 let state = accounts
12960 .accounts
12961 .entry(self.account_id.clone())
12962 .or_default();
12963 state.web_acls.retain(|_, v| v.arn != physical_id);
12964 Ok(())
12965 }
12966
12967 fn create_wafv2_ip_set(
12968 &self,
12969 resource: &ResourceDefinition,
12970 ) -> Result<ProvisionResult, String> {
12971 let props = &resource.properties;
12972 let name = props
12973 .get("Name")
12974 .and_then(|v| v.as_str())
12975 .ok_or("Name is required")?
12976 .to_string();
12977 let scope = props
12978 .get("Scope")
12979 .and_then(|v| v.as_str())
12980 .ok_or("Scope is required")?
12981 .to_string();
12982 let ip_address_version = props
12983 .get("IPAddressVersion")
12984 .and_then(|v| v.as_str())
12985 .ok_or("IPAddressVersion is required")?
12986 .to_string();
12987 let addresses: Vec<String> = props
12988 .get("Addresses")
12989 .and_then(|v| v.as_array())
12990 .map(|arr| {
12991 arr.iter()
12992 .filter_map(|v| v.as_str().map(String::from))
12993 .collect()
12994 })
12995 .unwrap_or_default();
12996 let description = props
12997 .get("Description")
12998 .and_then(|v| v.as_str())
12999 .map(String::from);
13000
13001 let id = Uuid::new_v4().to_string();
13002 let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
13003 ("us-east-1", "global".to_string())
13004 } else {
13005 (self.region.as_str(), self.region.clone())
13006 };
13007 let arn = format!(
13008 "arn:aws:wafv2:{}:{}:{}/ipset/{}/{}",
13009 region_in_arn, self.account_id, scope_seg, name, id
13010 );
13011 let ip_set = IpSet {
13012 id: id.clone(),
13013 name: name.clone(),
13014 arn: arn.clone(),
13015 scope: scope.clone(),
13016 description,
13017 ip_address_version,
13018 addresses,
13019 lock_token: Uuid::new_v4().simple().to_string(),
13020 created_time: Utc::now(),
13021 };
13022
13023 let mut accounts = self.wafv2_state.write();
13024 let state = accounts
13025 .accounts
13026 .entry(self.account_id.clone())
13027 .or_default();
13028 state.ip_sets.insert((scope, name.clone()), ip_set);
13029
13030 Ok(ProvisionResult::new(arn.clone())
13031 .with("Arn", arn)
13032 .with("Id", id)
13033 .with("Name", name))
13034 }
13035
13036 fn delete_wafv2_ip_set(&self, physical_id: &str) -> Result<(), String> {
13037 let mut accounts = self.wafv2_state.write();
13038 let state = accounts
13039 .accounts
13040 .entry(self.account_id.clone())
13041 .or_default();
13042 state.ip_sets.retain(|_, v| v.arn != physical_id);
13043 Ok(())
13044 }
13045
13046 fn create_wafv2_regex_pattern_set(
13047 &self,
13048 resource: &ResourceDefinition,
13049 ) -> Result<ProvisionResult, String> {
13050 let props = &resource.properties;
13051 let name = props
13052 .get("Name")
13053 .and_then(|v| v.as_str())
13054 .ok_or("Name is required")?
13055 .to_string();
13056 let scope = props
13057 .get("Scope")
13058 .and_then(|v| v.as_str())
13059 .ok_or("Scope is required")?
13060 .to_string();
13061 let regular_expressions: Vec<serde_json::Value> = props
13062 .get("RegularExpressionList")
13063 .and_then(|v| v.as_array())
13064 .map(|arr| {
13065 arr.iter()
13066 .map(|s| {
13067 if let Some(s) = s.as_str() {
13068 serde_json::json!({"RegexString": s})
13069 } else {
13070 s.clone()
13071 }
13072 })
13073 .collect()
13074 })
13075 .unwrap_or_default();
13076 let description = props
13077 .get("Description")
13078 .and_then(|v| v.as_str())
13079 .map(String::from);
13080
13081 let id = Uuid::new_v4().to_string();
13082 let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
13083 ("us-east-1", "global".to_string())
13084 } else {
13085 (self.region.as_str(), self.region.clone())
13086 };
13087 let arn = format!(
13088 "arn:aws:wafv2:{}:{}:{}/regexpatternset/{}/{}",
13089 region_in_arn, self.account_id, scope_seg, name, id
13090 );
13091 let set = RegexPatternSet {
13092 id: id.clone(),
13093 name: name.clone(),
13094 arn: arn.clone(),
13095 scope: scope.clone(),
13096 description,
13097 regular_expressions,
13098 lock_token: Uuid::new_v4().simple().to_string(),
13099 created_time: Utc::now(),
13100 };
13101
13102 let mut accounts = self.wafv2_state.write();
13103 let state = accounts
13104 .accounts
13105 .entry(self.account_id.clone())
13106 .or_default();
13107 state.regex_pattern_sets.insert((scope, name.clone()), set);
13108
13109 Ok(ProvisionResult::new(arn.clone())
13110 .with("Arn", arn)
13111 .with("Id", id)
13112 .with("Name", name))
13113 }
13114
13115 fn delete_wafv2_regex_pattern_set(&self, physical_id: &str) -> Result<(), String> {
13116 let mut accounts = self.wafv2_state.write();
13117 let state = accounts
13118 .accounts
13119 .entry(self.account_id.clone())
13120 .or_default();
13121 state.regex_pattern_sets.retain(|_, v| v.arn != physical_id);
13122 Ok(())
13123 }
13124
13125 fn create_wafv2_rule_group(
13126 &self,
13127 resource: &ResourceDefinition,
13128 ) -> Result<ProvisionResult, String> {
13129 let props = &resource.properties;
13130 let name = props
13131 .get("Name")
13132 .and_then(|v| v.as_str())
13133 .ok_or("Name is required")?
13134 .to_string();
13135 let scope = props
13136 .get("Scope")
13137 .and_then(|v| v.as_str())
13138 .ok_or("Scope is required")?
13139 .to_string();
13140 let capacity = props
13141 .get("Capacity")
13142 .and_then(|v| v.as_i64())
13143 .ok_or("Capacity is required")?;
13144 let description = props
13145 .get("Description")
13146 .and_then(|v| v.as_str())
13147 .map(String::from);
13148 let rules = props
13149 .get("Rules")
13150 .and_then(|v| v.as_array())
13151 .cloned()
13152 .unwrap_or_default();
13153 let visibility_config = props
13154 .get("VisibilityConfig")
13155 .cloned()
13156 .unwrap_or_else(|| serde_json::json!({}));
13157
13158 let id = Uuid::new_v4().to_string();
13159 let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
13160 ("us-east-1", "global".to_string())
13161 } else {
13162 (self.region.as_str(), self.region.clone())
13163 };
13164 let arn = format!(
13165 "arn:aws:wafv2:{}:{}:{}/rulegroup/{}/{}",
13166 region_in_arn, self.account_id, scope_seg, name, id
13167 );
13168 let rg = RuleGroup {
13169 id: id.clone(),
13170 name: name.clone(),
13171 arn: arn.clone(),
13172 scope: scope.clone(),
13173 capacity,
13174 description,
13175 rules,
13176 visibility_config,
13177 lock_token: Uuid::new_v4().simple().to_string(),
13178 label_namespace: format!("awswaf:{}:rulegroup:{}:", self.account_id, name),
13179 custom_response_bodies: BTreeMap::new(),
13180 available_labels: Vec::new(),
13181 consumed_labels: Vec::new(),
13182 created_time: Utc::now(),
13183 };
13184
13185 let mut accounts = self.wafv2_state.write();
13186 let state = accounts
13187 .accounts
13188 .entry(self.account_id.clone())
13189 .or_default();
13190 state.rule_groups.insert((scope, name.clone()), rg);
13191
13192 Ok(ProvisionResult::new(arn.clone())
13193 .with("Arn", arn)
13194 .with("Id", id)
13195 .with("Name", name)
13196 .with("Capacity", capacity.to_string()))
13197 }
13198
13199 fn delete_wafv2_rule_group(&self, physical_id: &str) -> Result<(), String> {
13200 let mut accounts = self.wafv2_state.write();
13201 let state = accounts
13202 .accounts
13203 .entry(self.account_id.clone())
13204 .or_default();
13205 state.rule_groups.retain(|_, v| v.arn != physical_id);
13206 Ok(())
13207 }
13208
13209 fn create_wafv2_logging_configuration(
13210 &self,
13211 resource: &ResourceDefinition,
13212 ) -> Result<ProvisionResult, String> {
13213 let props = &resource.properties;
13214 let resource_arn = props
13215 .get("ResourceArn")
13216 .and_then(|v| v.as_str())
13217 .ok_or("ResourceArn is required")?
13218 .to_string();
13219 let cfg = serde_json::json!({
13220 "ResourceArn": resource_arn,
13221 "LogDestinationConfigs": props.get("LogDestinationConfigs").cloned().unwrap_or_else(|| serde_json::json!([])),
13222 "RedactedFields": props.get("RedactedFields").cloned().unwrap_or_else(|| serde_json::json!([])),
13223 "LoggingFilter": props.get("LoggingFilter").cloned(),
13224 });
13225
13226 let mut accounts = self.wafv2_state.write();
13227 let state = accounts
13228 .accounts
13229 .entry(self.account_id.clone())
13230 .or_default();
13231 state.logging_configs.insert(resource_arn.clone(), cfg);
13232
13233 Ok(ProvisionResult::new(resource_arn))
13234 }
13235
13236 fn delete_wafv2_logging_configuration(&self, physical_id: &str) -> Result<(), String> {
13237 let mut accounts = self.wafv2_state.write();
13238 let state = accounts
13239 .accounts
13240 .entry(self.account_id.clone())
13241 .or_default();
13242 state.logging_configs.remove(physical_id);
13243 Ok(())
13244 }
13245
13246 fn create_wafv2_web_acl_association(
13247 &self,
13248 resource: &ResourceDefinition,
13249 ) -> Result<ProvisionResult, String> {
13250 let props = &resource.properties;
13251 let resource_arn = props
13252 .get("ResourceArn")
13253 .and_then(|v| v.as_str())
13254 .ok_or("ResourceArn is required")?
13255 .to_string();
13256 let web_acl_arn = props
13257 .get("WebACLArn")
13258 .and_then(|v| v.as_str())
13259 .ok_or("WebACLArn is required")?
13260 .to_string();
13261
13262 let mut accounts = self.wafv2_state.write();
13263 let state = accounts
13264 .accounts
13265 .entry(self.account_id.clone())
13266 .or_default();
13267 state.associations.insert(resource_arn.clone(), web_acl_arn);
13268
13269 Ok(ProvisionResult::new(resource_arn))
13271 }
13272
13273 fn delete_wafv2_web_acl_association(&self, physical_id: &str) -> Result<(), String> {
13274 let mut accounts = self.wafv2_state.write();
13275 let state = accounts
13276 .accounts
13277 .entry(self.account_id.clone())
13278 .or_default();
13279 state.associations.remove(physical_id);
13280 Ok(())
13281 }
13282
13283 fn create_apigw_rest_api(
13286 &self,
13287 resource: &ResourceDefinition,
13288 ) -> Result<ProvisionResult, String> {
13289 let props = &resource.properties;
13290 let name = props
13291 .get("Name")
13292 .and_then(|v| v.as_str())
13293 .ok_or("Name is required")?
13294 .to_string();
13295 let description = props
13296 .get("Description")
13297 .and_then(|v| v.as_str())
13298 .map(String::from);
13299 let api_key_source = props
13300 .get("ApiKeySourceType")
13301 .and_then(|v| v.as_str())
13302 .unwrap_or("HEADER")
13303 .to_string();
13304 let endpoint_configuration = props
13305 .get("EndpointConfiguration")
13306 .cloned()
13307 .unwrap_or_else(|| serde_json::json!({"types": ["EDGE"]}));
13308 let policy = props
13309 .get("Policy")
13310 .map(|v| v.to_string().trim_matches('"').to_string());
13311 let binary_media_types: Vec<String> = props
13312 .get("BinaryMediaTypes")
13313 .and_then(|v| v.as_array())
13314 .map(|arr| {
13315 arr.iter()
13316 .filter_map(|v| v.as_str().map(String::from))
13317 .collect()
13318 })
13319 .unwrap_or_default();
13320 let minimum_compression_size = props.get("MinimumCompressionSize").and_then(|v| v.as_i64());
13321 let disable_execute_api_endpoint = props
13322 .get("DisableExecuteApiEndpoint")
13323 .and_then(|v| v.as_bool())
13324 .unwrap_or(false);
13325 let import_source = if props.get("Body").is_some() {
13329 Some("Body".to_string())
13330 } else if props.get("BodyS3Location").is_some() {
13331 Some("BodyS3Location".to_string())
13332 } else if props.get("CloneFrom").is_some() {
13333 Some("CloneFrom".to_string())
13334 } else {
13335 None
13336 };
13337 let tags = parse_acm_tags(props.get("Tags"));
13338
13339 let id = apigw_make_id();
13340 let root_resource_id = apigw_make_id();
13341 let now = Utc::now();
13342
13343 let api = ApiGwRestApi {
13344 id: id.clone(),
13345 name,
13346 description,
13347 version: props
13348 .get("Version")
13349 .and_then(|v| v.as_str())
13350 .map(String::from),
13351 created_date: now,
13352 api_key_source,
13353 endpoint_configuration,
13354 policy,
13355 binary_media_types,
13356 minimum_compression_size,
13357 disable_execute_api_endpoint,
13358 root_resource_id: root_resource_id.clone(),
13359 tags,
13360 import_source,
13361 };
13362
13363 let mut accounts = self.apigateway_state.write();
13364 let state = accounts.get_or_create(&self.account_id);
13365 state.apis.insert(id.clone(), api);
13366 let mut resources = BTreeMap::new();
13367 resources.insert(
13368 root_resource_id.clone(),
13369 ApiGwResource {
13370 id: root_resource_id.clone(),
13371 parent_id: None,
13372 path_part: None,
13373 path: "/".to_string(),
13374 },
13375 );
13376 state.resources.insert(id.clone(), resources);
13377
13378 Ok(ProvisionResult::new(id.clone())
13379 .with("RestApiId", id.clone())
13380 .with("RootResourceId", root_resource_id))
13381 }
13382
13383 fn update_apigw_rest_api(
13384 &self,
13385 existing: &StackResource,
13386 resource: &ResourceDefinition,
13387 ) -> Result<ProvisionResult, String> {
13388 let props = &resource.properties;
13389 let id = existing.physical_id.clone();
13390 let mut accounts = self.apigateway_state.write();
13391 let state = accounts.get_or_create(&self.account_id);
13392 let api = state
13393 .apis
13394 .get_mut(&id)
13395 .ok_or_else(|| format!("RestApi {id} not found for update"))?;
13396 if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
13397 api.name = name.to_string();
13398 }
13399 if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
13400 api.description = Some(desc.to_string());
13401 }
13402 if let Some(source) = props.get("ApiKeySourceType").and_then(|v| v.as_str()) {
13403 api.api_key_source = source.to_string();
13404 }
13405 if let Some(ep) = props.get("EndpointConfiguration").cloned() {
13406 api.endpoint_configuration = ep;
13407 }
13408 if let Some(arr) = props.get("BinaryMediaTypes").and_then(|v| v.as_array()) {
13409 api.binary_media_types = arr
13410 .iter()
13411 .filter_map(|v| v.as_str().map(String::from))
13412 .collect();
13413 }
13414 if let Some(size) = props.get("MinimumCompressionSize").and_then(|v| v.as_i64()) {
13415 api.minimum_compression_size = Some(size);
13416 }
13417 if let Some(b) = props
13418 .get("DisableExecuteApiEndpoint")
13419 .and_then(|v| v.as_bool())
13420 {
13421 api.disable_execute_api_endpoint = b;
13422 }
13423 if props.get("Tags").is_some() {
13424 api.tags = parse_acm_tags(props.get("Tags"));
13425 }
13426 let root = api.root_resource_id.clone();
13427 Ok(ProvisionResult::new(id.clone())
13428 .with("RestApiId", id)
13429 .with("RootResourceId", root))
13430 }
13431
13432 fn delete_apigw_rest_api(&self, physical_id: &str) -> Result<(), String> {
13433 let mut accounts = self.apigateway_state.write();
13434 let state = accounts.get_or_create(&self.account_id);
13435 state.apis.remove(physical_id);
13436 state.resources.remove(physical_id);
13437 let prefix = format!("{physical_id}/");
13438 state.methods.retain(|k, _| !k.starts_with(&prefix));
13439 state.integrations.retain(|k, _| !k.starts_with(&prefix));
13440 state
13441 .integration_responses
13442 .retain(|k, _| !k.starts_with(&prefix));
13443 state
13444 .method_responses
13445 .retain(|k, _| !k.starts_with(&prefix));
13446 state.deployments.remove(physical_id);
13447 state.stages.remove(physical_id);
13448 state.models.remove(physical_id);
13449 state.request_validators.remove(physical_id);
13450 state.authorizers.remove(physical_id);
13451 state.gateway_responses.remove(physical_id);
13452 Ok(())
13453 }
13454
13455 fn create_apigw_resource(
13456 &self,
13457 resource: &ResourceDefinition,
13458 ) -> Result<ProvisionResult, String> {
13459 let props = &resource.properties;
13460 let rest_api_id = props
13461 .get("RestApiId")
13462 .and_then(|v| v.as_str())
13463 .ok_or("RestApiId is required")?
13464 .to_string();
13465 let parent_id = props
13466 .get("ParentId")
13467 .and_then(|v| v.as_str())
13468 .ok_or("ParentId is required")?
13469 .to_string();
13470 let path_part = props
13471 .get("PathPart")
13472 .and_then(|v| v.as_str())
13473 .ok_or("PathPart is required")?
13474 .to_string();
13475
13476 let mut accounts = self.apigateway_state.write();
13477 let state = accounts.get_or_create(&self.account_id);
13478 let api_resources = state
13479 .resources
13480 .get(&rest_api_id)
13481 .ok_or_else(|| format!("RestApi {rest_api_id} not found"))?;
13482 let parent = api_resources
13483 .get(&parent_id)
13484 .ok_or_else(|| format!("Parent resource {parent_id} not found"))?;
13485 let parent_path = parent.path.clone();
13486 let path = if parent_path == "/" {
13487 format!("/{path_part}")
13488 } else {
13489 format!("{parent_path}/{path_part}")
13490 };
13491
13492 let id = apigw_make_id();
13493 let new_resource = ApiGwResource {
13494 id: id.clone(),
13495 parent_id: Some(parent_id),
13496 path_part: Some(path_part),
13497 path,
13498 };
13499 state
13500 .resources
13501 .entry(rest_api_id.clone())
13502 .or_default()
13503 .insert(id.clone(), new_resource);
13504
13505 Ok(ProvisionResult::new(id.clone())
13506 .with("ResourceId", id)
13507 .with("RestApiId", rest_api_id))
13508 }
13509
13510 fn delete_apigw_resource(
13511 &self,
13512 physical_id: &str,
13513 attributes: &BTreeMap<String, String>,
13514 ) -> Result<(), String> {
13515 let Some(rest_api_id) = attributes.get("RestApiId") else {
13516 return Ok(());
13517 };
13518 let mut accounts = self.apigateway_state.write();
13519 let state = accounts.get_or_create(&self.account_id);
13520 if let Some(map) = state.resources.get_mut(rest_api_id) {
13521 map.remove(physical_id);
13522 }
13523 let prefix = format!("{rest_api_id}/{physical_id}/");
13524 state.methods.retain(|k, _| !k.starts_with(&prefix));
13525 state.integrations.retain(|k, _| !k.starts_with(&prefix));
13526 Ok(())
13527 }
13528
13529 fn create_apigw_method(
13530 &self,
13531 resource: &ResourceDefinition,
13532 ) -> Result<ProvisionResult, String> {
13533 let props = &resource.properties;
13534 let rest_api_id = props
13535 .get("RestApiId")
13536 .and_then(|v| v.as_str())
13537 .ok_or("RestApiId is required")?
13538 .to_string();
13539 let resource_id = props
13540 .get("ResourceId")
13541 .and_then(|v| v.as_str())
13542 .ok_or("ResourceId is required")?
13543 .to_string();
13544 let http_method = props
13545 .get("HttpMethod")
13546 .and_then(|v| v.as_str())
13547 .ok_or("HttpMethod is required")?
13548 .to_uppercase();
13549 let authorization_type = props
13550 .get("AuthorizationType")
13551 .and_then(|v| v.as_str())
13552 .unwrap_or("NONE")
13553 .to_string();
13554 let authorizer_id = props
13555 .get("AuthorizerId")
13556 .and_then(|v| v.as_str())
13557 .map(String::from);
13558 let api_key_required = props
13559 .get("ApiKeyRequired")
13560 .and_then(|v| v.as_bool())
13561 .unwrap_or(false);
13562 let operation_name = props
13563 .get("OperationName")
13564 .and_then(|v| v.as_str())
13565 .map(String::from);
13566 let request_validator_id = props
13567 .get("RequestValidatorId")
13568 .and_then(|v| v.as_str())
13569 .map(String::from);
13570 let request_parameters: BTreeMap<String, bool> = props
13571 .get("RequestParameters")
13572 .and_then(|v| v.as_object())
13573 .map(|obj| {
13574 obj.iter()
13575 .map(|(k, v)| (k.clone(), v.as_bool().unwrap_or(false)))
13576 .collect()
13577 })
13578 .unwrap_or_default();
13579 let request_models: BTreeMap<String, String> = props
13580 .get("RequestModels")
13581 .and_then(|v| v.as_object())
13582 .map(|obj| {
13583 obj.iter()
13584 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
13585 .collect()
13586 })
13587 .unwrap_or_default();
13588 let authorization_scopes: Vec<String> = props
13589 .get("AuthorizationScopes")
13590 .and_then(|v| v.as_array())
13591 .map(|arr| {
13592 arr.iter()
13593 .filter_map(|v| v.as_str().map(String::from))
13594 .collect()
13595 })
13596 .unwrap_or_default();
13597
13598 let composite_key = format!("{rest_api_id}/{resource_id}/{http_method}");
13599 let method = ApiGwMethod {
13600 rest_api_id: rest_api_id.clone(),
13601 resource_id: resource_id.clone(),
13602 http_method: http_method.clone(),
13603 authorization_type,
13604 authorizer_id,
13605 api_key_required,
13606 operation_name,
13607 request_parameters,
13608 request_models,
13609 request_validator_id,
13610 authorization_scopes,
13611 };
13612
13613 let mut accounts = self.apigateway_state.write();
13614 let state = accounts.get_or_create(&self.account_id);
13615 if !state.apis.contains_key(&rest_api_id) {
13616 return Err(format!("RestApi {rest_api_id} not found"));
13617 }
13618 let resource_known = state
13622 .resources
13623 .get(&rest_api_id)
13624 .map(|m| m.contains_key(&resource_id))
13625 .unwrap_or(false);
13626 if !resource_known {
13627 return Err(format!(
13628 "Resource {resource_id} not yet provisioned for api {rest_api_id}"
13629 ));
13630 }
13631 state.methods.insert(composite_key.clone(), method);
13632
13633 if let Some(integ_props) = props.get("Integration").and_then(|v| v.as_object()) {
13634 let integration = ApiGwIntegration {
13635 rest_api_id: rest_api_id.clone(),
13636 resource_id: resource_id.clone(),
13637 http_method: http_method.clone(),
13638 integration_type: integ_props
13639 .get("Type")
13640 .and_then(|v| v.as_str())
13641 .unwrap_or("MOCK")
13642 .to_string(),
13643 integration_http_method: integ_props
13644 .get("IntegrationHttpMethod")
13645 .and_then(|v| v.as_str())
13646 .map(String::from),
13647 uri: integ_props
13648 .get("Uri")
13649 .and_then(|v| v.as_str())
13650 .map(String::from),
13651 credentials: integ_props
13652 .get("Credentials")
13653 .and_then(|v| v.as_str())
13654 .map(String::from),
13655 request_parameters: integ_props
13656 .get("RequestParameters")
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 request_templates: integ_props
13665 .get("RequestTemplates")
13666 .and_then(|v| v.as_object())
13667 .map(|obj| {
13668 obj.iter()
13669 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
13670 .collect()
13671 })
13672 .unwrap_or_default(),
13673 passthrough_behavior: integ_props
13674 .get("PassthroughBehavior")
13675 .and_then(|v| v.as_str())
13676 .unwrap_or("WHEN_NO_MATCH")
13677 .to_string(),
13678 timeout_in_millis: integ_props
13679 .get("TimeoutInMillis")
13680 .and_then(|v| v.as_i64())
13681 .map(|n| n as i32),
13682 cache_namespace: integ_props
13683 .get("CacheNamespace")
13684 .and_then(|v| v.as_str())
13685 .map(String::from),
13686 cache_key_parameters: integ_props
13687 .get("CacheKeyParameters")
13688 .and_then(|v| v.as_array())
13689 .map(|arr| {
13690 arr.iter()
13691 .filter_map(|v| v.as_str().map(String::from))
13692 .collect()
13693 })
13694 .unwrap_or_default(),
13695 content_handling: integ_props
13696 .get("ContentHandling")
13697 .and_then(|v| v.as_str())
13698 .map(String::from),
13699 connection_type: integ_props
13700 .get("ConnectionType")
13701 .and_then(|v| v.as_str())
13702 .map(String::from),
13703 connection_id: integ_props
13704 .get("ConnectionId")
13705 .and_then(|v| v.as_str())
13706 .map(String::from),
13707 tls_config: integ_props.get("TlsConfig").cloned(),
13708 };
13709 state
13710 .integrations
13711 .insert(composite_key.clone(), integration);
13712 }
13713
13714 Ok(ProvisionResult::new(composite_key.clone())
13715 .with("MethodKey", composite_key)
13716 .with("RestApiId", rest_api_id)
13717 .with("ResourceId", resource_id)
13718 .with("HttpMethod", http_method))
13719 }
13720
13721 fn delete_apigw_method(&self, physical_id: &str) -> Result<(), String> {
13722 let mut accounts = self.apigateway_state.write();
13723 let state = accounts.get_or_create(&self.account_id);
13724 state.methods.remove(physical_id);
13725 state.integrations.remove(physical_id);
13726 let prefix = format!("{physical_id}/");
13727 state
13728 .integration_responses
13729 .retain(|k, _| !k.starts_with(&prefix));
13730 state
13731 .method_responses
13732 .retain(|k, _| !k.starts_with(&prefix));
13733 Ok(())
13734 }
13735
13736 fn create_apigw_deployment(
13737 &self,
13738 resource: &ResourceDefinition,
13739 ) -> Result<ProvisionResult, String> {
13740 let props = &resource.properties;
13741 let rest_api_id = props
13742 .get("RestApiId")
13743 .and_then(|v| v.as_str())
13744 .ok_or("RestApiId is required")?
13745 .to_string();
13746 let description = props
13747 .get("Description")
13748 .and_then(|v| v.as_str())
13749 .map(String::from);
13750
13751 let id = apigw_make_id();
13752 let mut accounts = self.apigateway_state.write();
13753 let state = accounts.get_or_create(&self.account_id);
13754 if !state.apis.contains_key(&rest_api_id) {
13755 return Err(format!("RestApi {rest_api_id} not found"));
13756 }
13757 let api_summary = serde_json::to_value(
13758 state
13759 .resources
13760 .get(&rest_api_id)
13761 .cloned()
13762 .unwrap_or_default(),
13763 )
13764 .unwrap_or(serde_json::Value::Null);
13765 let deployment = ApiGwDeployment {
13766 id: id.clone(),
13767 description,
13768 created_date: Utc::now(),
13769 api_summary,
13770 };
13771 state
13772 .deployments
13773 .entry(rest_api_id.clone())
13774 .or_default()
13775 .insert(id.clone(), deployment);
13776
13777 if let Some(stage_name) = props
13779 .get("StageName")
13780 .and_then(|v| v.as_str())
13781 .map(String::from)
13782 {
13783 let stage = ApiGwStage {
13784 stage_name: stage_name.clone(),
13785 deployment_id: id.clone(),
13786 description: props
13787 .get("StageDescription")
13788 .and_then(|v| v.get("Description"))
13789 .and_then(|v| v.as_str())
13790 .map(String::from),
13791 cache_cluster_enabled: false,
13792 cache_cluster_size: None,
13793 variables: BTreeMap::new(),
13794 method_settings: BTreeMap::new(),
13795 created_date: Utc::now(),
13796 last_updated_date: Utc::now(),
13797 tracing_enabled: false,
13798 web_acl_arn: None,
13799 canary_settings: None,
13800 access_log_settings: None,
13801 tags: BTreeMap::new(),
13802 };
13803 state
13804 .stages
13805 .entry(rest_api_id.clone())
13806 .or_default()
13807 .insert(stage_name, stage);
13808 }
13809
13810 Ok(ProvisionResult::new(id.clone())
13811 .with("DeploymentId", id)
13812 .with("RestApiId", rest_api_id))
13813 }
13814
13815 fn delete_apigw_deployment(
13816 &self,
13817 physical_id: &str,
13818 attributes: &BTreeMap<String, String>,
13819 ) -> Result<(), String> {
13820 let Some(rest_api_id) = attributes.get("RestApiId") else {
13821 return Ok(());
13822 };
13823 let mut accounts = self.apigateway_state.write();
13824 let state = accounts.get_or_create(&self.account_id);
13825 if let Some(map) = state.deployments.get_mut(rest_api_id) {
13826 map.remove(physical_id);
13827 }
13828 Ok(())
13829 }
13830
13831 fn create_apigw_stage(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
13832 let props = &resource.properties;
13833 let rest_api_id = props
13834 .get("RestApiId")
13835 .and_then(|v| v.as_str())
13836 .ok_or("RestApiId is required")?
13837 .to_string();
13838 let stage_name = props
13839 .get("StageName")
13840 .and_then(|v| v.as_str())
13841 .ok_or("StageName is required")?
13842 .to_string();
13843 let deployment_id = props
13844 .get("DeploymentId")
13845 .and_then(|v| v.as_str())
13846 .ok_or("DeploymentId is required")?
13847 .to_string();
13848
13849 let variables: BTreeMap<String, String> = props
13850 .get("Variables")
13851 .and_then(|v| v.as_object())
13852 .map(|obj| {
13853 obj.iter()
13854 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
13855 .collect()
13856 })
13857 .unwrap_or_default();
13858 let tracing_enabled = props
13859 .get("TracingEnabled")
13860 .and_then(|v| v.as_bool())
13861 .unwrap_or(false);
13862 let cache_cluster_enabled = props
13863 .get("CacheClusterEnabled")
13864 .and_then(|v| v.as_bool())
13865 .unwrap_or(false);
13866 let cache_cluster_size = props
13867 .get("CacheClusterSize")
13868 .and_then(|v| v.as_str())
13869 .map(String::from);
13870 let method_settings: BTreeMap<String, serde_json::Value> = props
13874 .get("MethodSettings")
13875 .and_then(|v| v.as_array())
13876 .map(|arr| {
13877 arr.iter()
13878 .filter_map(|s| {
13879 let path = s.get("ResourcePath").and_then(|v| v.as_str())?;
13880 let http = s.get("HttpMethod").and_then(|v| v.as_str())?;
13881 let key = format!("{}/{http}", path.strip_prefix('/').unwrap_or(path));
13882 Some((key, s.clone()))
13883 })
13884 .collect()
13885 })
13886 .unwrap_or_default();
13887 let tags = parse_acm_tags(props.get("Tags"));
13888
13889 let stage = ApiGwStage {
13890 stage_name: stage_name.clone(),
13891 deployment_id,
13892 description: props
13893 .get("Description")
13894 .and_then(|v| v.as_str())
13895 .map(String::from),
13896 cache_cluster_enabled,
13897 cache_cluster_size,
13898 variables,
13899 method_settings,
13900 created_date: Utc::now(),
13901 last_updated_date: Utc::now(),
13902 tracing_enabled,
13903 web_acl_arn: None,
13904 canary_settings: props.get("CanarySetting").cloned(),
13905 access_log_settings: props.get("AccessLogSetting").cloned(),
13906 tags,
13907 };
13908
13909 let mut accounts = self.apigateway_state.write();
13910 let state = accounts.get_or_create(&self.account_id);
13911 if !state.apis.contains_key(&rest_api_id) {
13912 return Err(format!("RestApi {rest_api_id} not found"));
13913 }
13914 let dep_known = state
13915 .deployments
13916 .get(&rest_api_id)
13917 .map(|m| m.contains_key(&stage.deployment_id))
13918 .unwrap_or(false);
13919 if !dep_known {
13920 return Err(format!(
13921 "Deployment {} not yet provisioned for api {rest_api_id}",
13922 stage.deployment_id
13923 ));
13924 }
13925 state
13926 .stages
13927 .entry(rest_api_id.clone())
13928 .or_default()
13929 .insert(stage_name.clone(), stage);
13930
13931 Ok(ProvisionResult::new(stage_name.clone())
13932 .with("StageName", stage_name)
13933 .with("RestApiId", rest_api_id))
13934 }
13935
13936 fn delete_apigw_stage(
13937 &self,
13938 physical_id: &str,
13939 attributes: &BTreeMap<String, String>,
13940 ) -> Result<(), String> {
13941 let Some(rest_api_id) = attributes.get("RestApiId") else {
13942 return Ok(());
13943 };
13944 let mut accounts = self.apigateway_state.write();
13945 let state = accounts.get_or_create(&self.account_id);
13946 if let Some(map) = state.stages.get_mut(rest_api_id) {
13947 map.remove(physical_id);
13948 }
13949 Ok(())
13950 }
13951
13952 fn create_apigw_authorizer(
13953 &self,
13954 resource: &ResourceDefinition,
13955 ) -> Result<ProvisionResult, String> {
13956 let props = &resource.properties;
13957 let rest_api_id = props
13958 .get("RestApiId")
13959 .and_then(|v| v.as_str())
13960 .ok_or("RestApiId is required")?
13961 .to_string();
13962 let name = props
13963 .get("Name")
13964 .and_then(|v| v.as_str())
13965 .ok_or("Name is required")?
13966 .to_string();
13967 let authorizer_type = props
13968 .get("Type")
13969 .and_then(|v| v.as_str())
13970 .unwrap_or("TOKEN")
13971 .to_string();
13972 let provider_arns: Vec<String> = props
13973 .get("ProviderARNs")
13974 .and_then(|v| v.as_array())
13975 .map(|arr| {
13976 arr.iter()
13977 .filter_map(|v| v.as_str().map(String::from))
13978 .collect()
13979 })
13980 .unwrap_or_default();
13981
13982 let id = apigw_make_id();
13983 let auth = ApiGwAuthorizer {
13984 id: id.clone(),
13985 name,
13986 authorizer_type,
13987 provider_arns,
13988 auth_type: props
13989 .get("AuthType")
13990 .and_then(|v| v.as_str())
13991 .map(String::from),
13992 authorizer_uri: props
13993 .get("AuthorizerUri")
13994 .and_then(|v| v.as_str())
13995 .map(String::from),
13996 authorizer_credentials: props
13997 .get("AuthorizerCredentials")
13998 .and_then(|v| v.as_str())
13999 .map(String::from),
14000 identity_source: props
14001 .get("IdentitySource")
14002 .and_then(|v| v.as_str())
14003 .map(String::from),
14004 identity_validation_expression: props
14005 .get("IdentityValidationExpression")
14006 .and_then(|v| v.as_str())
14007 .map(String::from),
14008 authorizer_result_ttl_in_seconds: props
14009 .get("AuthorizerResultTtlInSeconds")
14010 .and_then(|v| v.as_i64())
14011 .map(|n| n as i32),
14012 };
14013
14014 let mut accounts = self.apigateway_state.write();
14015 let state = accounts.get_or_create(&self.account_id);
14016 if !state.apis.contains_key(&rest_api_id) {
14017 return Err(format!("RestApi {rest_api_id} not found"));
14018 }
14019 state
14020 .authorizers
14021 .entry(rest_api_id.clone())
14022 .or_default()
14023 .insert(id.clone(), auth);
14024
14025 Ok(ProvisionResult::new(id.clone())
14026 .with("AuthorizerId", id)
14027 .with("RestApiId", rest_api_id))
14028 }
14029
14030 fn delete_apigw_authorizer(
14031 &self,
14032 physical_id: &str,
14033 attributes: &BTreeMap<String, String>,
14034 ) -> Result<(), String> {
14035 let Some(rest_api_id) = attributes.get("RestApiId") else {
14036 return Ok(());
14037 };
14038 let mut accounts = self.apigateway_state.write();
14039 let state = accounts.get_or_create(&self.account_id);
14040 if let Some(map) = state.authorizers.get_mut(rest_api_id) {
14041 map.remove(physical_id);
14042 }
14043 Ok(())
14044 }
14045
14046 fn create_apigw_request_validator(
14047 &self,
14048 resource: &ResourceDefinition,
14049 ) -> Result<ProvisionResult, String> {
14050 let props = &resource.properties;
14051 let rest_api_id = props
14052 .get("RestApiId")
14053 .and_then(|v| v.as_str())
14054 .ok_or("RestApiId is required")?
14055 .to_string();
14056 let name = props.get("Name").and_then(|v| v.as_str()).map(String::from);
14057 let validate_body = props
14058 .get("ValidateRequestBody")
14059 .and_then(|v| v.as_bool())
14060 .unwrap_or(false);
14061 let validate_params = props
14062 .get("ValidateRequestParameters")
14063 .and_then(|v| v.as_bool())
14064 .unwrap_or(false);
14065 let id = apigw_make_id();
14066 let body = serde_json::json!({
14067 "id": id,
14068 "name": name,
14069 "validateRequestBody": validate_body,
14070 "validateRequestParameters": validate_params,
14071 });
14072 let mut accounts = self.apigateway_state.write();
14073 let state = accounts.get_or_create(&self.account_id);
14074 state
14075 .request_validators
14076 .entry(rest_api_id.clone())
14077 .or_default()
14078 .insert(id.clone(), body);
14079 Ok(ProvisionResult::new(id.clone())
14080 .with("RequestValidatorId", id)
14081 .with("RestApiId", rest_api_id))
14082 }
14083
14084 fn delete_apigw_request_validator(
14085 &self,
14086 physical_id: &str,
14087 attributes: &BTreeMap<String, String>,
14088 ) -> Result<(), String> {
14089 let Some(rest_api_id) = attributes.get("RestApiId") else {
14090 return Ok(());
14091 };
14092 let mut accounts = self.apigateway_state.write();
14093 let state = accounts.get_or_create(&self.account_id);
14094 if let Some(map) = state.request_validators.get_mut(rest_api_id) {
14095 map.remove(physical_id);
14096 }
14097 Ok(())
14098 }
14099
14100 fn create_apigw_model(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
14101 let props = &resource.properties;
14102 let rest_api_id = props
14103 .get("RestApiId")
14104 .and_then(|v| v.as_str())
14105 .ok_or("RestApiId is required")?
14106 .to_string();
14107 let name = props
14108 .get("Name")
14109 .and_then(|v| v.as_str())
14110 .ok_or("Name is required")?
14111 .to_string();
14112 let content_type = props
14113 .get("ContentType")
14114 .and_then(|v| v.as_str())
14115 .unwrap_or("application/json")
14116 .to_string();
14117 let schema = props.get("Schema").map(|v| {
14118 if let Some(s) = v.as_str() {
14119 s.to_string()
14120 } else {
14121 v.to_string()
14122 }
14123 });
14124 let id = apigw_make_id();
14125 let model = ApiGwModel {
14126 id: id.clone(),
14127 name: name.clone(),
14128 description: props
14129 .get("Description")
14130 .and_then(|v| v.as_str())
14131 .map(String::from),
14132 schema,
14133 content_type,
14134 };
14135 let mut accounts = self.apigateway_state.write();
14136 let state = accounts.get_or_create(&self.account_id);
14137 state
14138 .models
14139 .entry(rest_api_id.clone())
14140 .or_default()
14141 .insert(name.clone(), model);
14142 Ok(ProvisionResult::new(name.clone())
14143 .with("ModelName", name)
14144 .with("RestApiId", rest_api_id))
14145 }
14146
14147 fn delete_apigw_model(
14148 &self,
14149 physical_id: &str,
14150 attributes: &BTreeMap<String, String>,
14151 ) -> Result<(), String> {
14152 let Some(rest_api_id) = attributes.get("RestApiId") else {
14153 return Ok(());
14154 };
14155 let mut accounts = self.apigateway_state.write();
14156 let state = accounts.get_or_create(&self.account_id);
14157 if let Some(map) = state.models.get_mut(rest_api_id) {
14158 map.remove(physical_id);
14159 }
14160 Ok(())
14161 }
14162
14163 fn create_apigw_gateway_response(
14164 &self,
14165 resource: &ResourceDefinition,
14166 ) -> Result<ProvisionResult, String> {
14167 let props = &resource.properties;
14168 let rest_api_id = props
14169 .get("RestApiId")
14170 .and_then(|v| v.as_str())
14171 .ok_or("RestApiId is required")?
14172 .to_string();
14173 let response_type = props
14174 .get("ResponseType")
14175 .and_then(|v| v.as_str())
14176 .ok_or("ResponseType is required")?
14177 .to_string();
14178 let body = serde_json::json!({
14179 "responseType": response_type,
14180 "statusCode": props.get("StatusCode").and_then(|v| v.as_str()),
14181 "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
14182 "responseTemplates": props.get("ResponseTemplates").cloned().unwrap_or(serde_json::json!({})),
14183 });
14184 let mut accounts = self.apigateway_state.write();
14185 let state = accounts.get_or_create(&self.account_id);
14186 state
14187 .gateway_responses
14188 .entry(rest_api_id.clone())
14189 .or_default()
14190 .insert(response_type.clone(), body);
14191 Ok(ProvisionResult::new(response_type.clone())
14192 .with("ResponseType", response_type)
14193 .with("RestApiId", rest_api_id))
14194 }
14195
14196 fn delete_apigw_gateway_response(
14197 &self,
14198 physical_id: &str,
14199 attributes: &BTreeMap<String, String>,
14200 ) -> Result<(), String> {
14201 let Some(rest_api_id) = attributes.get("RestApiId") else {
14202 return Ok(());
14203 };
14204 let mut accounts = self.apigateway_state.write();
14205 let state = accounts.get_or_create(&self.account_id);
14206 if let Some(map) = state.gateway_responses.get_mut(rest_api_id) {
14207 map.remove(physical_id);
14208 }
14209 Ok(())
14210 }
14211
14212 fn create_apigw_usage_plan(
14213 &self,
14214 resource: &ResourceDefinition,
14215 ) -> Result<ProvisionResult, String> {
14216 let props = &resource.properties;
14217 let name = props
14218 .get("UsagePlanName")
14219 .and_then(|v| v.as_str())
14220 .ok_or("UsagePlanName is required")?
14221 .to_string();
14222 let id = apigw_make_id();
14223 let plan = ApiGwUsagePlan {
14224 id: id.clone(),
14225 name,
14226 description: props
14227 .get("Description")
14228 .and_then(|v| v.as_str())
14229 .map(String::from),
14230 api_stages: props
14231 .get("ApiStages")
14232 .and_then(|v| v.as_array())
14233 .cloned()
14234 .unwrap_or_default()
14235 .into_iter()
14236 .map(lowercase_first_keys)
14237 .collect(),
14238 throttle: props.get("Throttle").cloned().map(lowercase_first_keys),
14239 quota: props.get("Quota").cloned().map(lowercase_first_keys),
14240 product_code: None,
14241 tags: parse_acm_tags(props.get("Tags")),
14242 };
14243 let mut accounts = self.apigateway_state.write();
14244 let state = accounts.get_or_create(&self.account_id);
14245 state.usage_plans.insert(id.clone(), plan);
14246 Ok(ProvisionResult::new(id.clone()).with("UsagePlanId", id))
14247 }
14248
14249 fn delete_apigw_usage_plan(&self, physical_id: &str) -> Result<(), String> {
14250 let mut accounts = self.apigateway_state.write();
14251 let state = accounts.get_or_create(&self.account_id);
14252 state.usage_plans.remove(physical_id);
14253 state.usage_plan_keys.remove(physical_id);
14254 Ok(())
14255 }
14256
14257 fn create_apigw_api_key(
14258 &self,
14259 resource: &ResourceDefinition,
14260 ) -> Result<ProvisionResult, String> {
14261 let props = &resource.properties;
14262 let generate_distinct_id = props
14263 .get("GenerateDistinctId")
14264 .and_then(|v| v.as_bool())
14265 .unwrap_or(false);
14266 let name = props
14267 .get("Name")
14268 .and_then(|v| v.as_str())
14269 .map(String::from)
14270 .unwrap_or_else(|| {
14271 if generate_distinct_id {
14272 format!("cfn-key-{}-{}", resource.logical_id, apigw_make_id())
14273 } else {
14274 format!("cfn-key-{}", resource.logical_id)
14275 }
14276 });
14277 let value = props
14278 .get("Value")
14279 .and_then(|v| v.as_str())
14280 .map(String::from)
14281 .unwrap_or_else(|| Uuid::new_v4().simple().to_string());
14282 let enabled = props
14283 .get("Enabled")
14284 .and_then(|v| v.as_bool())
14285 .unwrap_or(true);
14286 let stage_keys: Vec<String> = props
14289 .get("StageKeys")
14290 .and_then(|v| v.as_array())
14291 .map(|arr| {
14292 arr.iter()
14293 .filter_map(|s| {
14294 let rest = s.get("RestApiId").and_then(|v| v.as_str())?;
14295 let stage = s.get("StageName").and_then(|v| v.as_str())?;
14296 Some(format!("{rest}/{stage}"))
14297 })
14298 .collect()
14299 })
14300 .unwrap_or_default();
14301 let id = apigw_make_id();
14302 let now = Utc::now();
14303 let key = ApiGwApiKey {
14304 id: id.clone(),
14305 value,
14306 name,
14307 description: props
14308 .get("Description")
14309 .and_then(|v| v.as_str())
14310 .map(String::from),
14311 enabled,
14312 created_date: now,
14313 last_updated_date: now,
14314 stage_keys,
14315 tags: parse_acm_tags(props.get("Tags")),
14316 customer_id: props
14317 .get("CustomerId")
14318 .and_then(|v| v.as_str())
14319 .map(String::from),
14320 };
14321 let mut accounts = self.apigateway_state.write();
14322 let state = accounts.get_or_create(&self.account_id);
14323 state.api_keys.insert(id.clone(), key);
14324 Ok(ProvisionResult::new(id.clone()).with("ApiKeyId", id))
14325 }
14326
14327 fn delete_apigw_api_key(&self, physical_id: &str) -> Result<(), String> {
14328 let mut accounts = self.apigateway_state.write();
14329 let state = accounts.get_or_create(&self.account_id);
14330 state.api_keys.remove(physical_id);
14331 Ok(())
14332 }
14333
14334 fn create_apigw_usage_plan_key(
14335 &self,
14336 resource: &ResourceDefinition,
14337 ) -> Result<ProvisionResult, String> {
14338 let props = &resource.properties;
14339 let usage_plan_id = props
14340 .get("UsagePlanId")
14341 .and_then(|v| v.as_str())
14342 .ok_or("UsagePlanId is required")?
14343 .to_string();
14344 let key_id = props
14345 .get("KeyId")
14346 .and_then(|v| v.as_str())
14347 .ok_or("KeyId is required")?
14348 .to_string();
14349 let key_type = props
14350 .get("KeyType")
14351 .and_then(|v| v.as_str())
14352 .unwrap_or("API_KEY")
14353 .to_string();
14354 let body = serde_json::json!({
14355 "id": key_id,
14356 "type": key_type,
14357 });
14358 let mut accounts = self.apigateway_state.write();
14359 let state = accounts.get_or_create(&self.account_id);
14360 if !state.usage_plans.contains_key(&usage_plan_id) {
14361 return Err(format!("UsagePlan {usage_plan_id} not yet provisioned"));
14362 }
14363 if !state.api_keys.contains_key(&key_id) {
14364 return Err(format!("ApiKey {key_id} not yet provisioned"));
14365 }
14366 state
14367 .usage_plan_keys
14368 .entry(usage_plan_id.clone())
14369 .or_default()
14370 .insert(key_id.clone(), body);
14371 let physical = format!("{usage_plan_id}/{key_id}");
14372 Ok(ProvisionResult::new(physical)
14373 .with("UsagePlanId", usage_plan_id)
14374 .with("KeyId", key_id))
14375 }
14376
14377 fn delete_apigw_usage_plan_key(
14378 &self,
14379 physical_id: &str,
14380 _attributes: &BTreeMap<String, String>,
14381 ) -> Result<(), String> {
14382 let mut parts = physical_id.splitn(2, '/');
14383 let Some(plan_id) = parts.next() else {
14384 return Ok(());
14385 };
14386 let Some(key_id) = parts.next() else {
14387 return Ok(());
14388 };
14389 let mut accounts = self.apigateway_state.write();
14390 let state = accounts.get_or_create(&self.account_id);
14391 if let Some(map) = state.usage_plan_keys.get_mut(plan_id) {
14392 map.remove(key_id);
14393 }
14394 Ok(())
14395 }
14396
14397 fn create_apigw_domain_name(
14398 &self,
14399 resource: &ResourceDefinition,
14400 ) -> Result<ProvisionResult, String> {
14401 let props = &resource.properties;
14402 let domain_name = props
14403 .get("DomainName")
14404 .and_then(|v| v.as_str())
14405 .ok_or("DomainName is required")?
14406 .to_string();
14407 let mtls = props
14408 .get("MutualTlsAuthentication")
14409 .cloned()
14410 .map(lowercase_first_keys);
14411 let regional_domain = format!(
14412 "d-{}.execute-api.{}.amazonaws.com",
14413 apigw_make_id(),
14414 self.region
14415 );
14416 let distribution_domain = format!("d{}.cloudfront.net", apigw_make_id());
14417 let body = serde_json::json!({
14418 "domainName": domain_name,
14419 "certificateArn": props.get("CertificateArn").and_then(|v| v.as_str()),
14420 "regionalCertificateArn": props.get("RegionalCertificateArn").and_then(|v| v.as_str()),
14421 "endpointConfiguration": props.get("EndpointConfiguration").cloned().unwrap_or(serde_json::json!({"types": ["EDGE"]})),
14422 "securityPolicy": props.get("SecurityPolicy").and_then(|v| v.as_str()),
14423 "ownershipVerificationCertificateArn": props.get("OwnershipVerificationCertificateArn").and_then(|v| v.as_str()),
14424 "regionalDomainName": regional_domain,
14425 "regionalHostedZoneId": "Z2FDTNDATAQYW2",
14426 "distributionDomainName": distribution_domain,
14427 "distributionHostedZoneId": "Z2FDTNDATAQYW2",
14428 "mutualTlsAuthentication": mtls,
14429 "tags": serde_json::Value::Object(
14430 parse_acm_tags(props.get("Tags"))
14431 .into_iter()
14432 .map(|(k, v)| (k, serde_json::Value::String(v)))
14433 .collect(),
14434 ),
14435 });
14436 let mut accounts = self.apigateway_state.write();
14437 let state = accounts.get_or_create(&self.account_id);
14438 state.domain_names.insert(domain_name.clone(), body);
14439 Ok(ProvisionResult::new(domain_name.clone())
14440 .with("DomainName", domain_name)
14441 .with("RegionalHostedZoneId", "Z2FDTNDATAQYW2".to_string())
14442 .with("DistributionHostedZoneId", "Z2FDTNDATAQYW2".to_string()))
14443 }
14444
14445 fn delete_apigw_domain_name(&self, physical_id: &str) -> Result<(), String> {
14446 let mut accounts = self.apigateway_state.write();
14447 let state = accounts.get_or_create(&self.account_id);
14448 state.domain_names.remove(physical_id);
14449 state.base_path_mappings.remove(physical_id);
14450 Ok(())
14451 }
14452
14453 fn create_apigw_base_path_mapping(
14454 &self,
14455 resource: &ResourceDefinition,
14456 ) -> Result<ProvisionResult, String> {
14457 let props = &resource.properties;
14458 let domain_name = props
14459 .get("DomainName")
14460 .and_then(|v| v.as_str())
14461 .ok_or("DomainName is required")?
14462 .to_string();
14463 let rest_api_id = props
14464 .get("RestApiId")
14465 .and_then(|v| v.as_str())
14466 .ok_or("RestApiId is required")?
14467 .to_string();
14468 let base_path = props
14469 .get("BasePath")
14470 .and_then(|v| v.as_str())
14471 .unwrap_or("(none)")
14472 .to_string();
14473 let stage = props
14474 .get("Stage")
14475 .and_then(|v| v.as_str())
14476 .map(String::from);
14477 let body = serde_json::json!({
14478 "basePath": base_path,
14479 "restApiId": rest_api_id,
14480 "stage": stage,
14481 });
14482 let mut accounts = self.apigateway_state.write();
14483 let state = accounts.get_or_create(&self.account_id);
14484 state
14485 .base_path_mappings
14486 .entry(domain_name.clone())
14487 .or_default()
14488 .insert(base_path.clone(), body);
14489 let physical = format!("{domain_name}/{base_path}");
14490 Ok(ProvisionResult::new(physical)
14491 .with("DomainName", domain_name)
14492 .with("BasePath", base_path))
14493 }
14494
14495 fn delete_apigw_base_path_mapping(
14496 &self,
14497 physical_id: &str,
14498 _attributes: &BTreeMap<String, String>,
14499 ) -> Result<(), String> {
14500 let mut parts = physical_id.splitn(2, '/');
14501 let Some(domain) = parts.next() else {
14502 return Ok(());
14503 };
14504 let Some(base_path) = parts.next() else {
14505 return Ok(());
14506 };
14507 let mut accounts = self.apigateway_state.write();
14508 let state = accounts.get_or_create(&self.account_id);
14509 if let Some(map) = state.base_path_mappings.get_mut(domain) {
14510 map.remove(base_path);
14511 }
14512 Ok(())
14513 }
14514
14515 fn update_apigw_resource(
14523 &self,
14524 existing: &StackResource,
14525 resource: &ResourceDefinition,
14526 ) -> Result<ProvisionResult, String> {
14527 let props = &resource.properties;
14528 let rest_api_id = existing
14529 .attributes
14530 .get("RestApiId")
14531 .cloned()
14532 .or_else(|| {
14533 props
14534 .get("RestApiId")
14535 .and_then(|v| v.as_str())
14536 .map(String::from)
14537 })
14538 .ok_or("RestApiId is required")?;
14539 let physical = existing.physical_id.clone();
14540 let mut accounts = self.apigateway_state.write();
14541 let state = accounts.get_or_create(&self.account_id);
14542 let api_resources = state
14543 .resources
14544 .get_mut(&rest_api_id)
14545 .ok_or_else(|| format!("RestApi {rest_api_id} not found"))?;
14546 if !api_resources.contains_key(&physical) {
14547 return Err(format!("Resource {physical} not found"));
14548 }
14549 if let Some(part) = props.get("PathPart").and_then(|v| v.as_str()) {
14550 let parent_id = api_resources
14552 .get(&physical)
14553 .and_then(|r| r.parent_id.clone());
14554 let parent_path = parent_id
14555 .as_ref()
14556 .and_then(|pid| api_resources.get(pid).map(|p| p.path.clone()))
14557 .unwrap_or_else(|| "/".to_string());
14558 let new_path = if parent_path == "/" {
14559 format!("/{part}")
14560 } else {
14561 format!("{parent_path}/{part}")
14562 };
14563 let res = api_resources
14564 .get_mut(&physical)
14565 .expect("contains_key checked above");
14566 res.path_part = Some(part.to_string());
14567 res.path = new_path;
14568 }
14569 Ok(ProvisionResult::new(physical.clone())
14570 .with("ResourceId", physical)
14571 .with("RestApiId", rest_api_id))
14572 }
14573
14574 fn update_apigw_method(
14575 &self,
14576 existing: &StackResource,
14577 resource: &ResourceDefinition,
14578 ) -> Result<ProvisionResult, String> {
14579 self.create_apigw_method(resource).map(|r| {
14585 ProvisionResult {
14587 physical_id: existing.physical_id.clone(),
14588 attributes: r.attributes,
14589 }
14590 })
14591 }
14592
14593 fn update_apigw_deployment(
14594 &self,
14595 existing: &StackResource,
14596 resource: &ResourceDefinition,
14597 ) -> Result<ProvisionResult, String> {
14598 let props = &resource.properties;
14599 let rest_api_id = existing
14600 .attributes
14601 .get("RestApiId")
14602 .cloned()
14603 .or_else(|| {
14604 props
14605 .get("RestApiId")
14606 .and_then(|v| v.as_str())
14607 .map(String::from)
14608 })
14609 .ok_or("RestApiId is required")?;
14610 let physical = existing.physical_id.clone();
14611 let mut accounts = self.apigateway_state.write();
14612 let state = accounts.get_or_create(&self.account_id);
14613 let dep = state
14614 .deployments
14615 .get_mut(&rest_api_id)
14616 .and_then(|m| m.get_mut(&physical))
14617 .ok_or_else(|| format!("Deployment {physical} not found"))?;
14618 if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
14619 dep.description = Some(desc.to_string());
14620 }
14621 Ok(ProvisionResult::new(physical.clone())
14622 .with("DeploymentId", physical)
14623 .with("RestApiId", rest_api_id))
14624 }
14625
14626 fn update_apigw_stage(
14627 &self,
14628 existing: &StackResource,
14629 resource: &ResourceDefinition,
14630 ) -> Result<ProvisionResult, String> {
14631 let props = &resource.properties;
14632 let rest_api_id = existing
14633 .attributes
14634 .get("RestApiId")
14635 .cloned()
14636 .or_else(|| {
14637 props
14638 .get("RestApiId")
14639 .and_then(|v| v.as_str())
14640 .map(String::from)
14641 })
14642 .ok_or("RestApiId is required")?;
14643 let stage_name = existing.physical_id.clone();
14644 let mut accounts = self.apigateway_state.write();
14645 let state = accounts.get_or_create(&self.account_id);
14646 let stage = state
14647 .stages
14648 .get_mut(&rest_api_id)
14649 .and_then(|m| m.get_mut(&stage_name))
14650 .ok_or_else(|| format!("Stage {stage_name} not found"))?;
14651 if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
14652 stage.description = Some(desc.to_string());
14653 }
14654 if let Some(b) = props.get("TracingEnabled").and_then(|v| v.as_bool()) {
14655 stage.tracing_enabled = b;
14656 }
14657 if let Some(b) = props.get("CacheClusterEnabled").and_then(|v| v.as_bool()) {
14658 stage.cache_cluster_enabled = b;
14659 }
14660 if let Some(s) = props.get("CacheClusterSize").and_then(|v| v.as_str()) {
14661 stage.cache_cluster_size = Some(s.to_string());
14662 }
14663 if let Some(obj) = props.get("Variables").and_then(|v| v.as_object()) {
14664 stage.variables = obj
14665 .iter()
14666 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
14667 .collect();
14668 }
14669 if let Some(dep) = props.get("DeploymentId").and_then(|v| v.as_str()) {
14670 stage.deployment_id = dep.to_string();
14671 }
14672 if props.get("Tags").is_some() {
14673 stage.tags = parse_acm_tags(props.get("Tags"));
14674 }
14675 if let Some(arr) = props.get("MethodSettings").and_then(|v| v.as_array()) {
14676 stage.method_settings = arr
14677 .iter()
14678 .filter_map(|s| {
14679 let path = s.get("ResourcePath").and_then(|v| v.as_str())?;
14680 let http = s.get("HttpMethod").and_then(|v| v.as_str())?;
14681 let key = format!("{}/{http}", path.strip_prefix('/').unwrap_or(path));
14682 Some((key, s.clone()))
14683 })
14684 .collect();
14685 }
14686 if let Some(canary) = props.get("CanarySetting").cloned() {
14687 stage.canary_settings = Some(canary);
14688 }
14689 if let Some(access) = props.get("AccessLogSetting").cloned() {
14690 stage.access_log_settings = Some(access);
14691 }
14692 stage.last_updated_date = Utc::now();
14693 Ok(ProvisionResult::new(stage_name.clone())
14694 .with("StageName", stage_name)
14695 .with("RestApiId", rest_api_id))
14696 }
14697
14698 fn update_apigw_authorizer(
14699 &self,
14700 existing: &StackResource,
14701 resource: &ResourceDefinition,
14702 ) -> Result<ProvisionResult, String> {
14703 let props = &resource.properties;
14704 let rest_api_id = existing
14705 .attributes
14706 .get("RestApiId")
14707 .cloned()
14708 .or_else(|| {
14709 props
14710 .get("RestApiId")
14711 .and_then(|v| v.as_str())
14712 .map(String::from)
14713 })
14714 .ok_or("RestApiId is required")?;
14715 let physical = existing.physical_id.clone();
14716 let mut accounts = self.apigateway_state.write();
14717 let state = accounts.get_or_create(&self.account_id);
14718 let auth = state
14719 .authorizers
14720 .get_mut(&rest_api_id)
14721 .and_then(|m| m.get_mut(&physical))
14722 .ok_or_else(|| format!("Authorizer {physical} not found"))?;
14723 if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
14724 auth.name = name.to_string();
14725 }
14726 if let Some(t) = props.get("Type").and_then(|v| v.as_str()) {
14727 auth.authorizer_type = t.to_string();
14728 }
14729 if let Some(uri) = props.get("AuthorizerUri").and_then(|v| v.as_str()) {
14730 auth.authorizer_uri = Some(uri.to_string());
14731 }
14732 if let Some(arr) = props.get("ProviderARNs").and_then(|v| v.as_array()) {
14733 auth.provider_arns = arr
14734 .iter()
14735 .filter_map(|v| v.as_str().map(String::from))
14736 .collect();
14737 }
14738 if let Some(s) = props.get("IdentitySource").and_then(|v| v.as_str()) {
14739 auth.identity_source = Some(s.to_string());
14740 }
14741 if let Some(s) = props
14742 .get("IdentityValidationExpression")
14743 .and_then(|v| v.as_str())
14744 {
14745 auth.identity_validation_expression = Some(s.to_string());
14746 }
14747 if let Some(n) = props
14748 .get("AuthorizerResultTtlInSeconds")
14749 .and_then(|v| v.as_i64())
14750 {
14751 auth.authorizer_result_ttl_in_seconds = Some(n as i32);
14752 }
14753 if let Some(s) = props.get("AuthType").and_then(|v| v.as_str()) {
14754 auth.auth_type = Some(s.to_string());
14755 }
14756 if let Some(s) = props.get("AuthorizerCredentials").and_then(|v| v.as_str()) {
14757 auth.authorizer_credentials = Some(s.to_string());
14758 }
14759 Ok(ProvisionResult::new(physical.clone())
14760 .with("AuthorizerId", physical)
14761 .with("RestApiId", rest_api_id))
14762 }
14763
14764 fn update_apigw_request_validator(
14765 &self,
14766 existing: &StackResource,
14767 resource: &ResourceDefinition,
14768 ) -> Result<ProvisionResult, String> {
14769 let props = &resource.properties;
14770 let rest_api_id = existing
14771 .attributes
14772 .get("RestApiId")
14773 .cloned()
14774 .or_else(|| {
14775 props
14776 .get("RestApiId")
14777 .and_then(|v| v.as_str())
14778 .map(String::from)
14779 })
14780 .ok_or("RestApiId is required")?;
14781 let physical = existing.physical_id.clone();
14782 let mut accounts = self.apigateway_state.write();
14783 let state = accounts.get_or_create(&self.account_id);
14784 let body = state
14785 .request_validators
14786 .get_mut(&rest_api_id)
14787 .and_then(|m| m.get_mut(&physical))
14788 .ok_or_else(|| format!("RequestValidator {physical} not found"))?;
14789 let obj = body.as_object_mut().ok_or("validator body not object")?;
14790 if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
14791 obj.insert("name".into(), serde_json::Value::String(name.into()));
14792 }
14793 if let Some(b) = props.get("ValidateRequestBody").and_then(|v| v.as_bool()) {
14794 obj.insert("validateRequestBody".into(), serde_json::Value::Bool(b));
14795 }
14796 if let Some(b) = props
14797 .get("ValidateRequestParameters")
14798 .and_then(|v| v.as_bool())
14799 {
14800 obj.insert(
14801 "validateRequestParameters".into(),
14802 serde_json::Value::Bool(b),
14803 );
14804 }
14805 Ok(ProvisionResult::new(physical.clone())
14806 .with("RequestValidatorId", physical)
14807 .with("RestApiId", rest_api_id))
14808 }
14809
14810 fn update_apigw_model(
14811 &self,
14812 existing: &StackResource,
14813 resource: &ResourceDefinition,
14814 ) -> Result<ProvisionResult, String> {
14815 let props = &resource.properties;
14816 let rest_api_id = existing
14817 .attributes
14818 .get("RestApiId")
14819 .cloned()
14820 .or_else(|| {
14821 props
14822 .get("RestApiId")
14823 .and_then(|v| v.as_str())
14824 .map(String::from)
14825 })
14826 .ok_or("RestApiId is required")?;
14827 let model_name = existing.physical_id.clone();
14828 let mut accounts = self.apigateway_state.write();
14829 let state = accounts.get_or_create(&self.account_id);
14830 let model = state
14831 .models
14832 .get_mut(&rest_api_id)
14833 .and_then(|m| m.get_mut(&model_name))
14834 .ok_or_else(|| format!("Model {model_name} not found"))?;
14835 if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
14836 model.description = Some(desc.to_string());
14837 }
14838 if let Some(s) = props.get("ContentType").and_then(|v| v.as_str()) {
14839 model.content_type = s.to_string();
14840 }
14841 if let Some(schema) = props.get("Schema") {
14842 model.schema = Some(if let Some(s) = schema.as_str() {
14843 s.to_string()
14844 } else {
14845 schema.to_string()
14846 });
14847 }
14848 Ok(ProvisionResult::new(model_name.clone())
14849 .with("ModelName", model_name)
14850 .with("RestApiId", rest_api_id))
14851 }
14852
14853 fn update_apigw_gateway_response(
14854 &self,
14855 existing: &StackResource,
14856 resource: &ResourceDefinition,
14857 ) -> Result<ProvisionResult, String> {
14858 let props = &resource.properties;
14859 let rest_api_id = existing
14860 .attributes
14861 .get("RestApiId")
14862 .cloned()
14863 .or_else(|| {
14864 props
14865 .get("RestApiId")
14866 .and_then(|v| v.as_str())
14867 .map(String::from)
14868 })
14869 .ok_or("RestApiId is required")?;
14870 let response_type = existing.physical_id.clone();
14871 let mut accounts = self.apigateway_state.write();
14872 let state = accounts.get_or_create(&self.account_id);
14873 let body = state
14874 .gateway_responses
14875 .get_mut(&rest_api_id)
14876 .and_then(|m| m.get_mut(&response_type))
14877 .ok_or_else(|| format!("GatewayResponse {response_type} not found"))?;
14878 let obj = body.as_object_mut().ok_or("response body not object")?;
14879 if let Some(s) = props.get("StatusCode").and_then(|v| v.as_str()) {
14880 obj.insert("statusCode".into(), serde_json::Value::String(s.into()));
14881 }
14882 if let Some(v) = props.get("ResponseParameters").cloned() {
14883 obj.insert("responseParameters".into(), v);
14884 }
14885 if let Some(v) = props.get("ResponseTemplates").cloned() {
14886 obj.insert("responseTemplates".into(), v);
14887 }
14888 Ok(ProvisionResult::new(response_type.clone())
14889 .with("ResponseType", response_type)
14890 .with("RestApiId", rest_api_id))
14891 }
14892
14893 fn update_apigw_usage_plan(
14894 &self,
14895 existing: &StackResource,
14896 resource: &ResourceDefinition,
14897 ) -> Result<ProvisionResult, String> {
14898 let props = &resource.properties;
14899 let physical = existing.physical_id.clone();
14900 let mut accounts = self.apigateway_state.write();
14901 let state = accounts.get_or_create(&self.account_id);
14902 let plan = state
14903 .usage_plans
14904 .get_mut(&physical)
14905 .ok_or_else(|| format!("UsagePlan {physical} not found"))?;
14906 if let Some(name) = props.get("UsagePlanName").and_then(|v| v.as_str()) {
14907 plan.name = name.to_string();
14908 }
14909 if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
14910 plan.description = Some(s.to_string());
14911 }
14912 if let Some(arr) = props.get("ApiStages").and_then(|v| v.as_array()) {
14913 plan.api_stages = arr.iter().cloned().map(lowercase_first_keys).collect();
14914 }
14915 if let Some(t) = props.get("Throttle").cloned() {
14916 plan.throttle = Some(lowercase_first_keys(t));
14917 }
14918 if let Some(q) = props.get("Quota").cloned() {
14919 plan.quota = Some(lowercase_first_keys(q));
14920 }
14921 if props.get("Tags").is_some() {
14922 plan.tags = parse_acm_tags(props.get("Tags"));
14923 }
14924 Ok(ProvisionResult::new(physical.clone()).with("UsagePlanId", physical))
14925 }
14926
14927 fn update_apigw_api_key(
14928 &self,
14929 existing: &StackResource,
14930 resource: &ResourceDefinition,
14931 ) -> Result<ProvisionResult, String> {
14932 let props = &resource.properties;
14933 let physical = existing.physical_id.clone();
14934 let mut accounts = self.apigateway_state.write();
14935 let state = accounts.get_or_create(&self.account_id);
14936 let key = state
14937 .api_keys
14938 .get_mut(&physical)
14939 .ok_or_else(|| format!("ApiKey {physical} not found"))?;
14940 if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
14941 key.name = name.to_string();
14942 }
14943 if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
14944 key.description = Some(s.to_string());
14945 }
14946 if let Some(b) = props.get("Enabled").and_then(|v| v.as_bool()) {
14947 key.enabled = b;
14948 }
14949 if let Some(s) = props.get("CustomerId").and_then(|v| v.as_str()) {
14950 key.customer_id = Some(s.to_string());
14951 }
14952 if props.get("Tags").is_some() {
14953 key.tags = parse_acm_tags(props.get("Tags"));
14954 }
14955 if let Some(arr) = props.get("StageKeys").and_then(|v| v.as_array()) {
14956 key.stage_keys = arr
14957 .iter()
14958 .filter_map(|s| {
14959 let rest = s.get("RestApiId").and_then(|v| v.as_str())?;
14960 let stage = s.get("StageName").and_then(|v| v.as_str())?;
14961 Some(format!("{rest}/{stage}"))
14962 })
14963 .collect();
14964 }
14965 key.last_updated_date = Utc::now();
14966 Ok(ProvisionResult::new(physical.clone()).with("ApiKeyId", physical))
14967 }
14968
14969 fn update_apigw_usage_plan_key(
14970 &self,
14971 existing: &StackResource,
14972 _resource: &ResourceDefinition,
14973 ) -> Result<ProvisionResult, String> {
14974 let physical = existing.physical_id.clone();
14979 let mut parts = physical.splitn(2, '/');
14980 let plan = parts.next().unwrap_or("").to_string();
14981 let key = parts.next().unwrap_or("").to_string();
14982 Ok(ProvisionResult::new(physical)
14983 .with("UsagePlanId", plan)
14984 .with("KeyId", key))
14985 }
14986
14987 fn update_apigw_domain_name(
14988 &self,
14989 existing: &StackResource,
14990 resource: &ResourceDefinition,
14991 ) -> Result<ProvisionResult, String> {
14992 let props = &resource.properties;
14993 let domain = existing.physical_id.clone();
14994 let mut accounts = self.apigateway_state.write();
14995 let state = accounts.get_or_create(&self.account_id);
14996 let body = state
14997 .domain_names
14998 .get_mut(&domain)
14999 .ok_or_else(|| format!("DomainName {domain} not found"))?;
15000 let obj = body.as_object_mut().ok_or("domain body not object")?;
15001 if let Some(s) = props.get("CertificateArn").and_then(|v| v.as_str()) {
15002 obj.insert("certificateArn".into(), serde_json::Value::String(s.into()));
15003 }
15004 if let Some(s) = props.get("RegionalCertificateArn").and_then(|v| v.as_str()) {
15005 obj.insert(
15006 "regionalCertificateArn".into(),
15007 serde_json::Value::String(s.into()),
15008 );
15009 }
15010 if let Some(v) = props.get("EndpointConfiguration").cloned() {
15011 obj.insert("endpointConfiguration".into(), v);
15012 }
15013 if let Some(s) = props.get("SecurityPolicy").and_then(|v| v.as_str()) {
15014 obj.insert("securityPolicy".into(), serde_json::Value::String(s.into()));
15015 }
15016 if let Some(v) = props.get("MutualTlsAuthentication").cloned() {
15017 obj.insert("mutualTlsAuthentication".into(), lowercase_first_keys(v));
15018 }
15019 if let Some(s) = props
15020 .get("OwnershipVerificationCertificateArn")
15021 .and_then(|v| v.as_str())
15022 {
15023 obj.insert(
15024 "ownershipVerificationCertificateArn".into(),
15025 serde_json::Value::String(s.into()),
15026 );
15027 }
15028 if props.get("Tags").is_some() {
15029 obj.insert(
15030 "tags".into(),
15031 serde_json::Value::Object(
15032 parse_acm_tags(props.get("Tags"))
15033 .into_iter()
15034 .map(|(k, v)| (k, serde_json::Value::String(v)))
15035 .collect(),
15036 ),
15037 );
15038 }
15039 Ok(ProvisionResult::new(domain.clone())
15040 .with("DomainName", domain)
15041 .with("RegionalHostedZoneId", "Z2FDTNDATAQYW2".to_string())
15042 .with("DistributionHostedZoneId", "Z2FDTNDATAQYW2".to_string()))
15043 }
15044
15045 fn update_apigw_base_path_mapping(
15046 &self,
15047 existing: &StackResource,
15048 resource: &ResourceDefinition,
15049 ) -> Result<ProvisionResult, String> {
15050 let props = &resource.properties;
15051 let physical = existing.physical_id.clone();
15052 let mut parts = physical.splitn(2, '/');
15053 let domain = parts
15054 .next()
15055 .ok_or("malformed base path mapping id")?
15056 .to_string();
15057 let base_path = parts
15058 .next()
15059 .ok_or("malformed base path mapping id")?
15060 .to_string();
15061 let mut accounts = self.apigateway_state.write();
15062 let state = accounts.get_or_create(&self.account_id);
15063 let map = state
15064 .base_path_mappings
15065 .get_mut(&domain)
15066 .ok_or_else(|| format!("DomainName {domain} not found"))?;
15067 let body = map
15068 .get_mut(&base_path)
15069 .ok_or_else(|| format!("BasePath {base_path} not found"))?;
15070 let obj = body.as_object_mut().ok_or("mapping body not object")?;
15071 if let Some(s) = props.get("RestApiId").and_then(|v| v.as_str()) {
15072 obj.insert("restApiId".into(), serde_json::Value::String(s.into()));
15073 }
15074 if let Some(s) = props.get("Stage").and_then(|v| v.as_str()) {
15075 obj.insert("stage".into(), serde_json::Value::String(s.into()));
15076 }
15077 Ok(ProvisionResult::new(physical)
15078 .with("DomainName", domain)
15079 .with("BasePath", base_path))
15080 }
15081
15082 fn create_apigwv2_api(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
15085 let props = &resource.properties;
15086 let name = props
15087 .get("Name")
15088 .and_then(|v| v.as_str())
15089 .ok_or("Name is required")?
15090 .to_string();
15091 let protocol_type = props
15092 .get("ProtocolType")
15093 .and_then(|v| v.as_str())
15094 .unwrap_or("HTTP")
15095 .to_string();
15096 let description = props
15097 .get("Description")
15098 .and_then(|v| v.as_str())
15099 .map(String::from);
15100 let tags: Option<BTreeMap<String, String>> =
15101 props.get("Tags").and_then(|v| v.as_object()).map(|obj| {
15102 obj.iter()
15103 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
15104 .collect()
15105 });
15106
15107 let id = make_apigwv2_id(10);
15108 let mut api = ApiGwV2HttpApi::new(id.clone(), name, description, tags, &self.region);
15109 api.protocol_type = protocol_type.clone();
15110 if let Some(expr) = props
15111 .get("RouteSelectionExpression")
15112 .and_then(|v| v.as_str())
15113 {
15114 api.route_selection_expression = expr.to_string();
15115 }
15116 if let Some(expr) = props
15117 .get("ApiKeySelectionExpression")
15118 .and_then(|v| v.as_str())
15119 {
15120 api.api_key_selection_expression = expr.to_string();
15121 }
15122 if let Some(b) = props
15123 .get("DisableExecuteApiEndpoint")
15124 .and_then(|v| v.as_bool())
15125 {
15126 api.disable_execute_api_endpoint = b;
15127 }
15128 if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
15129 api.ip_address_type = s.to_string();
15130 }
15131 if let Some(cors) = props.get("CorsConfiguration").and_then(|v| v.as_object()) {
15132 api.cors_configuration = Some(ApiGwV2CorsConfiguration {
15133 allow_credentials: cors.get("AllowCredentials").and_then(|v| v.as_bool()),
15134 allow_headers: cors
15135 .get("AllowHeaders")
15136 .and_then(|v| v.as_array())
15137 .map(|a| {
15138 a.iter()
15139 .filter_map(|v| v.as_str().map(String::from))
15140 .collect()
15141 }),
15142 allow_methods: cors
15143 .get("AllowMethods")
15144 .and_then(|v| v.as_array())
15145 .map(|a| {
15146 a.iter()
15147 .filter_map(|v| v.as_str().map(String::from))
15148 .collect()
15149 }),
15150 allow_origins: cors
15151 .get("AllowOrigins")
15152 .and_then(|v| v.as_array())
15153 .map(|a| {
15154 a.iter()
15155 .filter_map(|v| v.as_str().map(String::from))
15156 .collect()
15157 }),
15158 expose_headers: cors
15159 .get("ExposeHeaders")
15160 .and_then(|v| v.as_array())
15161 .map(|a| {
15162 a.iter()
15163 .filter_map(|v| v.as_str().map(String::from))
15164 .collect()
15165 }),
15166 max_age: cors
15167 .get("MaxAge")
15168 .and_then(|v| v.as_i64())
15169 .map(|n| n as i32),
15170 });
15171 }
15172
15173 let api_endpoint = api.api_endpoint.clone();
15174 let mut accounts = self.apigatewayv2_state.write();
15175 let state = accounts.get_or_create(&self.account_id);
15176 state.apis.insert(id.clone(), api);
15177
15178 Ok(ProvisionResult::new(id.clone())
15179 .with("ApiId", id)
15180 .with("ApiEndpoint", api_endpoint))
15181 }
15182
15183 fn delete_apigwv2_api(&self, physical_id: &str) -> Result<(), String> {
15184 let mut accounts = self.apigatewayv2_state.write();
15185 let state = accounts.get_or_create(&self.account_id);
15186 state.apis.remove(physical_id);
15187 state.routes.remove(physical_id);
15188 state.integrations.remove(physical_id);
15189 state.stages.remove(physical_id);
15190 state.deployments.remove(physical_id);
15191 state.authorizers.remove(physical_id);
15192 state.models.remove(physical_id);
15193 state.integration_responses.remove(physical_id);
15194 state.route_responses.remove(physical_id);
15195 Ok(())
15196 }
15197
15198 fn create_apigwv2_route(
15199 &self,
15200 resource: &ResourceDefinition,
15201 ) -> Result<ProvisionResult, String> {
15202 let props = &resource.properties;
15203 let api_id = props
15204 .get("ApiId")
15205 .and_then(|v| v.as_str())
15206 .ok_or("ApiId is required")?
15207 .to_string();
15208 let route_key = props
15209 .get("RouteKey")
15210 .and_then(|v| v.as_str())
15211 .ok_or("RouteKey is required")?
15212 .to_string();
15213
15214 let mut accounts = self.apigatewayv2_state.write();
15215 let state = accounts.get_or_create(&self.account_id);
15216 if !state.apis.contains_key(&api_id) {
15217 return Err(format!("Api {api_id} not yet provisioned"));
15218 }
15219 let id = make_apigwv2_id(10);
15220 let route = ApiGwV2Route {
15221 route_id: id.clone(),
15222 route_key,
15223 target: props
15224 .get("Target")
15225 .and_then(|v| v.as_str())
15226 .map(String::from),
15227 authorization_type: props
15228 .get("AuthorizationType")
15229 .and_then(|v| v.as_str())
15230 .map(String::from),
15231 authorizer_id: props
15232 .get("AuthorizerId")
15233 .and_then(|v| v.as_str())
15234 .map(String::from),
15235 };
15236 state
15237 .routes
15238 .entry(api_id.clone())
15239 .or_default()
15240 .insert(id.clone(), route);
15241
15242 Ok(ProvisionResult::new(id.clone())
15243 .with("RouteId", id)
15244 .with("ApiId", api_id))
15245 }
15246
15247 fn delete_apigwv2_route(
15248 &self,
15249 physical_id: &str,
15250 attributes: &BTreeMap<String, String>,
15251 ) -> Result<(), String> {
15252 let Some(api_id) = attributes.get("ApiId") else {
15253 return Ok(());
15254 };
15255 let mut accounts = self.apigatewayv2_state.write();
15256 let state = accounts.get_or_create(&self.account_id);
15257 if let Some(map) = state.routes.get_mut(api_id) {
15258 map.remove(physical_id);
15259 }
15260 Ok(())
15261 }
15262
15263 fn create_apigwv2_integration(
15264 &self,
15265 resource: &ResourceDefinition,
15266 ) -> Result<ProvisionResult, String> {
15267 let props = &resource.properties;
15268 let api_id = props
15269 .get("ApiId")
15270 .and_then(|v| v.as_str())
15271 .ok_or("ApiId is required")?
15272 .to_string();
15273 let integration_type = props
15274 .get("IntegrationType")
15275 .and_then(|v| v.as_str())
15276 .ok_or("IntegrationType is required")?
15277 .to_string();
15278
15279 let mut accounts = self.apigatewayv2_state.write();
15280 let state = accounts.get_or_create(&self.account_id);
15281 if !state.apis.contains_key(&api_id) {
15282 return Err(format!("Api {api_id} not yet provisioned"));
15283 }
15284 let id = make_apigwv2_id(10);
15285 let integration = ApiGwV2Integration {
15286 integration_id: id.clone(),
15287 integration_type,
15288 integration_uri: props
15289 .get("IntegrationUri")
15290 .and_then(|v| v.as_str())
15291 .map(String::from),
15292 payload_format_version: props
15293 .get("PayloadFormatVersion")
15294 .and_then(|v| v.as_str())
15295 .map(String::from),
15296 timeout_in_millis: props.get("TimeoutInMillis").and_then(|v| v.as_i64()),
15297 };
15298 state
15299 .integrations
15300 .entry(api_id.clone())
15301 .or_default()
15302 .insert(id.clone(), integration);
15303
15304 Ok(ProvisionResult::new(id.clone())
15305 .with("IntegrationId", id)
15306 .with("ApiId", api_id))
15307 }
15308
15309 fn delete_apigwv2_integration(
15310 &self,
15311 physical_id: &str,
15312 attributes: &BTreeMap<String, String>,
15313 ) -> Result<(), String> {
15314 let Some(api_id) = attributes.get("ApiId") else {
15315 return Ok(());
15316 };
15317 let mut accounts = self.apigatewayv2_state.write();
15318 let state = accounts.get_or_create(&self.account_id);
15319 if let Some(map) = state.integrations.get_mut(api_id) {
15320 map.remove(physical_id);
15321 }
15322 Ok(())
15323 }
15324
15325 fn create_apigwv2_integration_response(
15326 &self,
15327 resource: &ResourceDefinition,
15328 ) -> Result<ProvisionResult, String> {
15329 let props = &resource.properties;
15330 let api_id = props
15331 .get("ApiId")
15332 .and_then(|v| v.as_str())
15333 .ok_or("ApiId is required")?
15334 .to_string();
15335 let integration_id = props
15336 .get("IntegrationId")
15337 .and_then(|v| v.as_str())
15338 .ok_or("IntegrationId is required")?
15339 .to_string();
15340 let key_expr = props
15341 .get("IntegrationResponseKey")
15342 .and_then(|v| v.as_str())
15343 .ok_or("IntegrationResponseKey is required")?
15344 .to_string();
15345 let id = make_apigwv2_id(10);
15346 let body = serde_json::json!({
15347 "integrationResponseId": id,
15348 "integrationId": integration_id,
15349 "integrationResponseKey": key_expr,
15350 "responseTemplates": props.get("ResponseTemplates").cloned().unwrap_or(serde_json::json!({})),
15351 "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
15352 "templateSelectionExpression": props.get("TemplateSelectionExpression").and_then(|v| v.as_str()),
15353 "contentHandlingStrategy": props.get("ContentHandlingStrategy").and_then(|v| v.as_str()),
15354 });
15355 let composite_key = format!("{integration_id}/{id}");
15356 let mut accounts = self.apigatewayv2_state.write();
15357 let state = accounts.get_or_create(&self.account_id);
15358 if !state
15359 .integrations
15360 .get(&api_id)
15361 .map(|m| m.contains_key(&integration_id))
15362 .unwrap_or(false)
15363 {
15364 return Err(format!(
15365 "Integration {integration_id} not yet provisioned for api {api_id}"
15366 ));
15367 }
15368 state
15369 .integration_responses
15370 .entry(api_id.clone())
15371 .or_default()
15372 .insert(composite_key.clone(), body);
15373 Ok(ProvisionResult::new(composite_key.clone())
15374 .with("IntegrationResponseId", id)
15375 .with("IntegrationId", integration_id)
15376 .with("ApiId", api_id))
15377 }
15378
15379 fn delete_apigwv2_integration_response(
15380 &self,
15381 physical_id: &str,
15382 attributes: &BTreeMap<String, String>,
15383 ) -> Result<(), String> {
15384 let Some(api_id) = attributes.get("ApiId") else {
15385 return Ok(());
15386 };
15387 let mut accounts = self.apigatewayv2_state.write();
15388 let state = accounts.get_or_create(&self.account_id);
15389 if let Some(map) = state.integration_responses.get_mut(api_id) {
15390 map.remove(physical_id);
15391 }
15392 Ok(())
15393 }
15394
15395 fn create_apigwv2_route_response(
15396 &self,
15397 resource: &ResourceDefinition,
15398 ) -> Result<ProvisionResult, String> {
15399 let props = &resource.properties;
15400 let api_id = props
15401 .get("ApiId")
15402 .and_then(|v| v.as_str())
15403 .ok_or("ApiId is required")?
15404 .to_string();
15405 let route_id = props
15406 .get("RouteId")
15407 .and_then(|v| v.as_str())
15408 .ok_or("RouteId is required")?
15409 .to_string();
15410 let key_expr = props
15411 .get("RouteResponseKey")
15412 .and_then(|v| v.as_str())
15413 .ok_or("RouteResponseKey is required")?
15414 .to_string();
15415 let id = make_apigwv2_id(10);
15416 let body = serde_json::json!({
15417 "routeResponseId": id,
15418 "routeId": route_id,
15419 "routeResponseKey": key_expr,
15420 "responseModels": props.get("ResponseModels").cloned().unwrap_or(serde_json::json!({})),
15421 "modelSelectionExpression": props.get("ModelSelectionExpression").and_then(|v| v.as_str()),
15422 "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
15423 });
15424 let composite = format!("{route_id}/{id}");
15425 let mut accounts = self.apigatewayv2_state.write();
15426 let state = accounts.get_or_create(&self.account_id);
15427 if !state
15428 .routes
15429 .get(&api_id)
15430 .map(|m| m.contains_key(&route_id))
15431 .unwrap_or(false)
15432 {
15433 return Err(format!(
15434 "Route {route_id} not yet provisioned for api {api_id}"
15435 ));
15436 }
15437 state
15438 .route_responses
15439 .entry(api_id.clone())
15440 .or_default()
15441 .insert(composite.clone(), body);
15442 Ok(ProvisionResult::new(composite.clone())
15443 .with("RouteResponseId", id)
15444 .with("RouteId", route_id)
15445 .with("ApiId", api_id))
15446 }
15447
15448 fn delete_apigwv2_route_response(
15449 &self,
15450 physical_id: &str,
15451 attributes: &BTreeMap<String, String>,
15452 ) -> Result<(), String> {
15453 let Some(api_id) = attributes.get("ApiId") else {
15454 return Ok(());
15455 };
15456 let mut accounts = self.apigatewayv2_state.write();
15457 let state = accounts.get_or_create(&self.account_id);
15458 if let Some(map) = state.route_responses.get_mut(api_id) {
15459 map.remove(physical_id);
15460 }
15461 Ok(())
15462 }
15463
15464 fn create_apigwv2_stage(
15465 &self,
15466 resource: &ResourceDefinition,
15467 ) -> Result<ProvisionResult, String> {
15468 let props = &resource.properties;
15469 let api_id = props
15470 .get("ApiId")
15471 .and_then(|v| v.as_str())
15472 .ok_or("ApiId is required")?
15473 .to_string();
15474 let stage_name = props
15475 .get("StageName")
15476 .and_then(|v| v.as_str())
15477 .ok_or("StageName is required")?
15478 .to_string();
15479 let auto_deploy = props
15480 .get("AutoDeploy")
15481 .and_then(|v| v.as_bool())
15482 .unwrap_or(false);
15483 let deployment_id = props
15484 .get("DeploymentId")
15485 .and_then(|v| v.as_str())
15486 .map(String::from);
15487
15488 let stage_variables = props
15489 .get("StageVariables")
15490 .and_then(|v| v.as_object())
15491 .map(|obj| {
15492 obj.iter()
15493 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
15494 .collect()
15495 });
15496
15497 let access_log_settings = props.get("AccessLogSettings").and_then(|v| {
15498 let destination_arn = v.get("DestinationArn")?.as_str()?.to_string();
15499 let format = v.get("Format").and_then(|f| f.as_str().map(String::from));
15500 Some(fakecloud_apigatewayv2::AccessLogSettings {
15501 destination_arn,
15502 format,
15503 })
15504 });
15505
15506 let stage = ApiGwV2Stage {
15507 stage_name: stage_name.clone(),
15508 description: props
15509 .get("Description")
15510 .and_then(|v| v.as_str())
15511 .map(String::from),
15512 deployment_id: deployment_id.clone(),
15513 auto_deploy,
15514 created_date: Utc::now(),
15515 last_updated_date: None,
15516 web_acl_arn: None,
15517 stage_variables,
15518 access_log_settings,
15519 };
15520
15521 let mut accounts = self.apigatewayv2_state.write();
15522 let state = accounts.get_or_create(&self.account_id);
15523 if !state.apis.contains_key(&api_id) {
15524 return Err(format!("Api {api_id} not yet provisioned"));
15525 }
15526 if let Some(dep) = &deployment_id {
15527 if !state
15528 .deployments
15529 .get(&api_id)
15530 .map(|m| m.contains_key(dep))
15531 .unwrap_or(false)
15532 {
15533 return Err(format!(
15534 "Deployment {dep} not yet provisioned for api {api_id}"
15535 ));
15536 }
15537 }
15538 state
15539 .stages
15540 .entry(api_id.clone())
15541 .or_default()
15542 .insert(stage_name.clone(), stage);
15543
15544 Ok(ProvisionResult::new(stage_name.clone())
15545 .with("StageName", stage_name)
15546 .with("ApiId", api_id))
15547 }
15548
15549 fn delete_apigwv2_stage(
15550 &self,
15551 physical_id: &str,
15552 attributes: &BTreeMap<String, String>,
15553 ) -> Result<(), String> {
15554 let Some(api_id) = attributes.get("ApiId") else {
15555 return Ok(());
15556 };
15557 let mut accounts = self.apigatewayv2_state.write();
15558 let state = accounts.get_or_create(&self.account_id);
15559 if let Some(map) = state.stages.get_mut(api_id) {
15560 map.remove(physical_id);
15561 }
15562 Ok(())
15563 }
15564
15565 fn create_apigwv2_deployment(
15566 &self,
15567 resource: &ResourceDefinition,
15568 ) -> Result<ProvisionResult, String> {
15569 let props = &resource.properties;
15570 let api_id = props
15571 .get("ApiId")
15572 .and_then(|v| v.as_str())
15573 .ok_or("ApiId is required")?
15574 .to_string();
15575 let id = make_apigwv2_id(10);
15576 let deployment = ApiGwV2Deployment {
15577 deployment_id: id.clone(),
15578 description: props
15579 .get("Description")
15580 .and_then(|v| v.as_str())
15581 .map(String::from),
15582 created_date: Utc::now(),
15583 auto_deployed: false,
15584 };
15585 let mut accounts = self.apigatewayv2_state.write();
15586 let state = accounts.get_or_create(&self.account_id);
15587 if !state.apis.contains_key(&api_id) {
15588 return Err(format!("Api {api_id} not yet provisioned"));
15589 }
15590 state
15591 .deployments
15592 .entry(api_id.clone())
15593 .or_default()
15594 .insert(id.clone(), deployment);
15595 Ok(ProvisionResult::new(id.clone())
15596 .with("DeploymentId", id)
15597 .with("ApiId", api_id))
15598 }
15599
15600 fn delete_apigwv2_deployment(
15601 &self,
15602 physical_id: &str,
15603 attributes: &BTreeMap<String, String>,
15604 ) -> Result<(), String> {
15605 let Some(api_id) = attributes.get("ApiId") else {
15606 return Ok(());
15607 };
15608 let mut accounts = self.apigatewayv2_state.write();
15609 let state = accounts.get_or_create(&self.account_id);
15610 if let Some(map) = state.deployments.get_mut(api_id) {
15611 map.remove(physical_id);
15612 }
15613 Ok(())
15614 }
15615
15616 fn create_apigwv2_authorizer(
15617 &self,
15618 resource: &ResourceDefinition,
15619 ) -> Result<ProvisionResult, String> {
15620 let props = &resource.properties;
15621 let api_id = props
15622 .get("ApiId")
15623 .and_then(|v| v.as_str())
15624 .ok_or("ApiId is required")?
15625 .to_string();
15626 let name = props
15627 .get("Name")
15628 .and_then(|v| v.as_str())
15629 .ok_or("Name is required")?
15630 .to_string();
15631 let authorizer_type = props
15632 .get("AuthorizerType")
15633 .and_then(|v| v.as_str())
15634 .unwrap_or("REQUEST")
15635 .to_string();
15636 let identity_source = props
15637 .get("IdentitySource")
15638 .and_then(|v| v.as_array())
15639 .map(|arr| {
15640 arr.iter()
15641 .filter_map(|v| v.as_str().map(String::from))
15642 .collect::<Vec<String>>()
15643 });
15644 let jwt_configuration = props
15645 .get("JwtConfiguration")
15646 .and_then(|v| v.as_object())
15647 .map(|obj| ApiGwV2JwtConfiguration {
15648 audience: obj.get("Audience").and_then(|v| v.as_array()).map(|a| {
15649 a.iter()
15650 .filter_map(|v| v.as_str().map(String::from))
15651 .collect()
15652 }),
15653 issuer: obj.get("Issuer").and_then(|v| v.as_str()).map(String::from),
15654 });
15655
15656 let id = make_apigwv2_id(10);
15657 let auth = ApiGwV2Authorizer {
15658 authorizer_id: id.clone(),
15659 name,
15660 authorizer_type,
15661 authorizer_uri: props
15662 .get("AuthorizerUri")
15663 .and_then(|v| v.as_str())
15664 .map(String::from),
15665 identity_source,
15666 jwt_configuration,
15667 };
15668 let mut accounts = self.apigatewayv2_state.write();
15669 let state = accounts.get_or_create(&self.account_id);
15670 if !state.apis.contains_key(&api_id) {
15671 return Err(format!("Api {api_id} not yet provisioned"));
15672 }
15673 state
15674 .authorizers
15675 .entry(api_id.clone())
15676 .or_default()
15677 .insert(id.clone(), auth);
15678 Ok(ProvisionResult::new(id.clone())
15679 .with("AuthorizerId", id)
15680 .with("ApiId", api_id))
15681 }
15682
15683 fn delete_apigwv2_authorizer(
15684 &self,
15685 physical_id: &str,
15686 attributes: &BTreeMap<String, String>,
15687 ) -> Result<(), String> {
15688 let Some(api_id) = attributes.get("ApiId") else {
15689 return Ok(());
15690 };
15691 let mut accounts = self.apigatewayv2_state.write();
15692 let state = accounts.get_or_create(&self.account_id);
15693 if let Some(map) = state.authorizers.get_mut(api_id) {
15694 map.remove(physical_id);
15695 }
15696 Ok(())
15697 }
15698
15699 fn create_apigwv2_domain_name(
15700 &self,
15701 resource: &ResourceDefinition,
15702 ) -> Result<ProvisionResult, String> {
15703 let props = &resource.properties;
15704 let domain_name = props
15705 .get("DomainName")
15706 .and_then(|v| v.as_str())
15707 .ok_or("DomainName is required")?
15708 .to_string();
15709 let body = serde_json::json!({
15710 "domainName": domain_name,
15711 "domainNameConfigurations": props.get("DomainNameConfigurations").cloned().unwrap_or(serde_json::json!([])),
15712 "mutualTlsAuthentication": props.get("MutualTlsAuthentication").cloned(),
15713 "apiMappingSelectionExpression": null,
15714 });
15715 let mut accounts = self.apigatewayv2_state.write();
15716 let state = accounts.get_or_create(&self.account_id);
15717 state.domain_names.insert(domain_name.clone(), body);
15718 Ok(ProvisionResult::new(domain_name.clone()).with("DomainName", domain_name))
15719 }
15720
15721 fn delete_apigwv2_domain_name(&self, physical_id: &str) -> Result<(), String> {
15722 let mut accounts = self.apigatewayv2_state.write();
15723 let state = accounts.get_or_create(&self.account_id);
15724 state.domain_names.remove(physical_id);
15725 state.api_mappings.remove(physical_id);
15726 Ok(())
15727 }
15728
15729 fn create_apigwv2_api_mapping(
15730 &self,
15731 resource: &ResourceDefinition,
15732 ) -> Result<ProvisionResult, String> {
15733 let props = &resource.properties;
15734 let domain_name = props
15735 .get("DomainName")
15736 .and_then(|v| v.as_str())
15737 .ok_or("DomainName is required")?
15738 .to_string();
15739 let api_id = props
15740 .get("ApiId")
15741 .and_then(|v| v.as_str())
15742 .ok_or("ApiId is required")?
15743 .to_string();
15744 let stage = props
15745 .get("Stage")
15746 .and_then(|v| v.as_str())
15747 .ok_or("Stage is required")?
15748 .to_string();
15749 let api_mapping_key = props
15750 .get("ApiMappingKey")
15751 .and_then(|v| v.as_str())
15752 .map(String::from);
15753 let id = make_apigwv2_id(10);
15754 let body = serde_json::json!({
15755 "apiMappingId": id,
15756 "apiId": api_id,
15757 "stage": stage,
15758 "apiMappingKey": api_mapping_key,
15759 });
15760 let mut accounts = self.apigatewayv2_state.write();
15761 let state = accounts.get_or_create(&self.account_id);
15762 if !state.domain_names.contains_key(&domain_name) {
15763 return Err(format!("DomainName {domain_name} not yet provisioned"));
15764 }
15765 if !state.apis.contains_key(&api_id) {
15766 return Err(format!("Api {api_id} not yet provisioned"));
15767 }
15768 state
15769 .api_mappings
15770 .entry(domain_name.clone())
15771 .or_default()
15772 .insert(id.clone(), body);
15773 Ok(ProvisionResult::new(id.clone())
15774 .with("ApiMappingId", id)
15775 .with("DomainName", domain_name))
15776 }
15777
15778 fn delete_apigwv2_api_mapping(
15779 &self,
15780 physical_id: &str,
15781 attributes: &BTreeMap<String, String>,
15782 ) -> Result<(), String> {
15783 let Some(domain) = attributes.get("DomainName") else {
15784 return Ok(());
15785 };
15786 let mut accounts = self.apigatewayv2_state.write();
15787 let state = accounts.get_or_create(&self.account_id);
15788 if let Some(map) = state.api_mappings.get_mut(domain) {
15789 map.remove(physical_id);
15790 }
15791 Ok(())
15792 }
15793
15794 fn create_apigwv2_vpc_link(
15795 &self,
15796 resource: &ResourceDefinition,
15797 ) -> Result<ProvisionResult, String> {
15798 let props = &resource.properties;
15799 let name = props
15800 .get("Name")
15801 .and_then(|v| v.as_str())
15802 .ok_or("Name is required")?
15803 .to_string();
15804 let id = make_apigwv2_id(10);
15805 let body = serde_json::json!({
15806 "vpcLinkId": id,
15807 "name": name,
15808 "subnetIds": props.get("SubnetIds").cloned().unwrap_or(serde_json::json!([])),
15809 "securityGroupIds": props.get("SecurityGroupIds").cloned().unwrap_or(serde_json::json!([])),
15810 "tags": props.get("Tags").cloned().unwrap_or(serde_json::json!({})),
15811 "vpcLinkStatus": "AVAILABLE",
15812 "vpcLinkVersion": "V2",
15813 "createdDate": Utc::now().to_rfc3339(),
15814 });
15815 let mut accounts = self.apigatewayv2_state.write();
15816 let state = accounts.get_or_create(&self.account_id);
15817 state.vpc_links.insert(id.clone(), body);
15818 Ok(ProvisionResult::new(id.clone()).with("VpcLinkId", id))
15819 }
15820
15821 fn delete_apigwv2_vpc_link(&self, physical_id: &str) -> Result<(), String> {
15822 let mut accounts = self.apigatewayv2_state.write();
15823 let state = accounts.get_or_create(&self.account_id);
15824 state.vpc_links.remove(physical_id);
15825 Ok(())
15826 }
15827
15828 fn create_apigwv2_model(
15829 &self,
15830 resource: &ResourceDefinition,
15831 ) -> Result<ProvisionResult, String> {
15832 let props = &resource.properties;
15833 let api_id = props
15834 .get("ApiId")
15835 .and_then(|v| v.as_str())
15836 .ok_or("ApiId is required")?
15837 .to_string();
15838 let name = props
15839 .get("Name")
15840 .and_then(|v| v.as_str())
15841 .ok_or("Name is required")?
15842 .to_string();
15843 let id = make_apigwv2_id(10);
15844 let body = serde_json::json!({
15845 "modelId": id,
15846 "name": name,
15847 "contentType": props.get("ContentType").and_then(|v| v.as_str()).unwrap_or("application/json"),
15848 "description": props.get("Description").and_then(|v| v.as_str()),
15849 "schema": props.get("Schema").map(|v| if let Some(s) = v.as_str() { s.to_string() } else { v.to_string() }),
15850 });
15851 let mut accounts = self.apigatewayv2_state.write();
15852 let state = accounts.get_or_create(&self.account_id);
15853 if !state.apis.contains_key(&api_id) {
15854 return Err(format!("Api {api_id} not yet provisioned"));
15855 }
15856 state
15857 .models
15858 .entry(api_id.clone())
15859 .or_default()
15860 .insert(id.clone(), body);
15861 Ok(ProvisionResult::new(id.clone())
15862 .with("ModelId", id)
15863 .with("ApiId", api_id))
15864 }
15865
15866 fn delete_apigwv2_model(
15867 &self,
15868 physical_id: &str,
15869 attributes: &BTreeMap<String, String>,
15870 ) -> Result<(), String> {
15871 let Some(api_id) = attributes.get("ApiId") else {
15872 return Ok(());
15873 };
15874 let mut accounts = self.apigatewayv2_state.write();
15875 let state = accounts.get_or_create(&self.account_id);
15876 if let Some(map) = state.models.get_mut(api_id) {
15877 map.remove(physical_id);
15878 }
15879 Ok(())
15880 }
15881
15882 fn update_apigwv2_api(
15886 &self,
15887 existing: &StackResource,
15888 resource: &ResourceDefinition,
15889 ) -> Result<ProvisionResult, String> {
15890 let props = &resource.properties;
15891 let api_id = existing.physical_id.clone();
15892 let mut accounts = self.apigatewayv2_state.write();
15893 let state = accounts.get_or_create(&self.account_id);
15894 let api = state
15895 .apis
15896 .get_mut(&api_id)
15897 .ok_or_else(|| format!("Api {api_id} no longer exists in state"))?;
15898
15899 if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
15900 api.name = s.to_string();
15901 }
15902 if let Some(s) = props.get("ProtocolType").and_then(|v| v.as_str()) {
15903 api.protocol_type = s.to_string();
15904 }
15905 api.description = props
15906 .get("Description")
15907 .and_then(|v| v.as_str())
15908 .map(String::from)
15909 .or_else(|| api.description.clone());
15910 if let Some(s) = props
15911 .get("RouteSelectionExpression")
15912 .and_then(|v| v.as_str())
15913 {
15914 api.route_selection_expression = s.to_string();
15915 }
15916 if let Some(s) = props
15917 .get("ApiKeySelectionExpression")
15918 .and_then(|v| v.as_str())
15919 {
15920 api.api_key_selection_expression = s.to_string();
15921 }
15922 if let Some(b) = props
15923 .get("DisableExecuteApiEndpoint")
15924 .and_then(|v| v.as_bool())
15925 {
15926 api.disable_execute_api_endpoint = b;
15927 }
15928 if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
15929 api.ip_address_type = s.to_string();
15930 }
15931 if let Some(cors) = props.get("CorsConfiguration").and_then(|v| v.as_object()) {
15932 api.cors_configuration = Some(ApiGwV2CorsConfiguration {
15933 allow_credentials: cors.get("AllowCredentials").and_then(|v| v.as_bool()),
15934 allow_headers: cors
15935 .get("AllowHeaders")
15936 .and_then(|v| v.as_array())
15937 .map(|a| {
15938 a.iter()
15939 .filter_map(|v| v.as_str().map(String::from))
15940 .collect()
15941 }),
15942 allow_methods: cors
15943 .get("AllowMethods")
15944 .and_then(|v| v.as_array())
15945 .map(|a| {
15946 a.iter()
15947 .filter_map(|v| v.as_str().map(String::from))
15948 .collect()
15949 }),
15950 allow_origins: cors
15951 .get("AllowOrigins")
15952 .and_then(|v| v.as_array())
15953 .map(|a| {
15954 a.iter()
15955 .filter_map(|v| v.as_str().map(String::from))
15956 .collect()
15957 }),
15958 expose_headers: cors
15959 .get("ExposeHeaders")
15960 .and_then(|v| v.as_array())
15961 .map(|a| {
15962 a.iter()
15963 .filter_map(|v| v.as_str().map(String::from))
15964 .collect()
15965 }),
15966 max_age: cors
15967 .get("MaxAge")
15968 .and_then(|v| v.as_i64())
15969 .map(|n| n as i32),
15970 });
15971 }
15972 if let Some(obj) = props.get("Tags").and_then(|v| v.as_object()) {
15973 let tags: BTreeMap<String, String> = obj
15974 .iter()
15975 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
15976 .collect();
15977 api.tags = Some(tags);
15978 }
15979
15980 let api_endpoint = api.api_endpoint.clone();
15981 Ok(ProvisionResult::new(api_id.clone())
15982 .with("ApiId", api_id)
15983 .with("ApiEndpoint", api_endpoint))
15984 }
15985
15986 fn update_apigwv2_route(
15990 &self,
15991 existing: &StackResource,
15992 resource: &ResourceDefinition,
15993 ) -> Result<ProvisionResult, String> {
15994 let props = &resource.properties;
15995 let api_id = props
15996 .get("ApiId")
15997 .and_then(|v| v.as_str())
15998 .ok_or("ApiId is required")?
15999 .to_string();
16000 let route_id = existing.physical_id.clone();
16001
16002 let mut accounts = self.apigatewayv2_state.write();
16003 let state = accounts.get_or_create(&self.account_id);
16004 let routes = state
16005 .routes
16006 .get_mut(&api_id)
16007 .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16008 let route = routes
16009 .get_mut(&route_id)
16010 .ok_or_else(|| format!("Route {route_id} not yet provisioned for api {api_id}"))?;
16011 if let Some(s) = props.get("RouteKey").and_then(|v| v.as_str()) {
16012 route.route_key = s.to_string();
16013 }
16014 if let Some(s) = props.get("Target").and_then(|v| v.as_str()) {
16015 route.target = Some(s.to_string());
16016 }
16017 if let Some(s) = props.get("AuthorizationType").and_then(|v| v.as_str()) {
16018 route.authorization_type = Some(s.to_string());
16019 }
16020 if let Some(s) = props.get("AuthorizerId").and_then(|v| v.as_str()) {
16021 route.authorizer_id = Some(s.to_string());
16022 }
16023 Ok(ProvisionResult::new(route_id.clone())
16024 .with("RouteId", route_id)
16025 .with("ApiId", api_id))
16026 }
16027
16028 fn update_apigwv2_integration(
16030 &self,
16031 existing: &StackResource,
16032 resource: &ResourceDefinition,
16033 ) -> Result<ProvisionResult, String> {
16034 let props = &resource.properties;
16035 let api_id = props
16036 .get("ApiId")
16037 .and_then(|v| v.as_str())
16038 .ok_or("ApiId is required")?
16039 .to_string();
16040 let integration_id = existing.physical_id.clone();
16041
16042 let mut accounts = self.apigatewayv2_state.write();
16043 let state = accounts.get_or_create(&self.account_id);
16044 let integrations = state
16045 .integrations
16046 .get_mut(&api_id)
16047 .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16048 let integ = integrations.get_mut(&integration_id).ok_or_else(|| {
16049 format!("Integration {integration_id} not yet provisioned for api {api_id}")
16050 })?;
16051 if let Some(s) = props.get("IntegrationType").and_then(|v| v.as_str()) {
16052 integ.integration_type = s.to_string();
16053 }
16054 if let Some(s) = props.get("IntegrationUri").and_then(|v| v.as_str()) {
16055 integ.integration_uri = Some(s.to_string());
16056 }
16057 if let Some(s) = props.get("PayloadFormatVersion").and_then(|v| v.as_str()) {
16058 integ.payload_format_version = Some(s.to_string());
16059 }
16060 if let Some(n) = props.get("TimeoutInMillis").and_then(|v| v.as_i64()) {
16061 integ.timeout_in_millis = Some(n);
16062 }
16063 Ok(ProvisionResult::new(integration_id.clone())
16064 .with("IntegrationId", integration_id)
16065 .with("ApiId", api_id))
16066 }
16067
16068 fn update_apigwv2_integration_response(
16072 &self,
16073 existing: &StackResource,
16074 resource: &ResourceDefinition,
16075 ) -> Result<ProvisionResult, String> {
16076 let props = &resource.properties;
16077 let api_id = props
16078 .get("ApiId")
16079 .and_then(|v| v.as_str())
16080 .ok_or("ApiId is required")?
16081 .to_string();
16082 let composite_key = existing.physical_id.clone();
16083 let (integration_id, response_id) = composite_key
16084 .split_once('/')
16085 .map(|(a, b)| (a.to_string(), b.to_string()))
16086 .ok_or_else(|| format!("Invalid IntegrationResponse physical id: {composite_key}"))?;
16087 let key_expr = props
16088 .get("IntegrationResponseKey")
16089 .and_then(|v| v.as_str())
16090 .ok_or("IntegrationResponseKey is required")?
16091 .to_string();
16092 let body = serde_json::json!({
16093 "integrationResponseId": response_id,
16094 "integrationId": integration_id,
16095 "integrationResponseKey": key_expr,
16096 "responseTemplates": props.get("ResponseTemplates").cloned().unwrap_or(serde_json::json!({})),
16097 "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
16098 "templateSelectionExpression": props.get("TemplateSelectionExpression").and_then(|v| v.as_str()),
16099 "contentHandlingStrategy": props.get("ContentHandlingStrategy").and_then(|v| v.as_str()),
16100 });
16101 let mut accounts = self.apigatewayv2_state.write();
16102 let state = accounts.get_or_create(&self.account_id);
16103 let map = state
16104 .integration_responses
16105 .get_mut(&api_id)
16106 .ok_or_else(|| format!("No integration responses found for api {api_id}"))?;
16107 if !map.contains_key(&composite_key) {
16108 return Err(format!(
16109 "IntegrationResponse {composite_key} not yet provisioned for api {api_id}"
16110 ));
16111 }
16112 map.insert(composite_key.clone(), body);
16113 Ok(ProvisionResult::new(composite_key)
16114 .with("IntegrationResponseId", response_id)
16115 .with("IntegrationId", integration_id)
16116 .with("ApiId", api_id))
16117 }
16118
16119 fn update_apigwv2_route_response(
16122 &self,
16123 existing: &StackResource,
16124 resource: &ResourceDefinition,
16125 ) -> Result<ProvisionResult, String> {
16126 let props = &resource.properties;
16127 let api_id = props
16128 .get("ApiId")
16129 .and_then(|v| v.as_str())
16130 .ok_or("ApiId is required")?
16131 .to_string();
16132 let composite = existing.physical_id.clone();
16133 let (route_id, response_id) = composite
16134 .split_once('/')
16135 .map(|(a, b)| (a.to_string(), b.to_string()))
16136 .ok_or_else(|| format!("Invalid RouteResponse physical id: {composite}"))?;
16137 let key_expr = props
16138 .get("RouteResponseKey")
16139 .and_then(|v| v.as_str())
16140 .ok_or("RouteResponseKey is required")?
16141 .to_string();
16142 let body = serde_json::json!({
16143 "routeResponseId": response_id,
16144 "routeId": route_id,
16145 "routeResponseKey": key_expr,
16146 "responseModels": props.get("ResponseModels").cloned().unwrap_or(serde_json::json!({})),
16147 "modelSelectionExpression": props.get("ModelSelectionExpression").and_then(|v| v.as_str()),
16148 "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
16149 });
16150 let mut accounts = self.apigatewayv2_state.write();
16151 let state = accounts.get_or_create(&self.account_id);
16152 let map = state
16153 .route_responses
16154 .get_mut(&api_id)
16155 .ok_or_else(|| format!("No route responses found for api {api_id}"))?;
16156 if !map.contains_key(&composite) {
16157 return Err(format!(
16158 "RouteResponse {composite} not yet provisioned for api {api_id}"
16159 ));
16160 }
16161 map.insert(composite.clone(), body);
16162 Ok(ProvisionResult::new(composite)
16163 .with("RouteResponseId", response_id)
16164 .with("RouteId", route_id)
16165 .with("ApiId", api_id))
16166 }
16167
16168 fn update_apigwv2_stage(
16172 &self,
16173 existing: &StackResource,
16174 resource: &ResourceDefinition,
16175 ) -> Result<ProvisionResult, String> {
16176 let props = &resource.properties;
16177 let api_id = props
16178 .get("ApiId")
16179 .and_then(|v| v.as_str())
16180 .ok_or("ApiId is required")?
16181 .to_string();
16182 let stage_name = existing.physical_id.clone();
16183
16184 let mut accounts = self.apigatewayv2_state.write();
16185 let state = accounts.get_or_create(&self.account_id);
16186 let stages = state
16187 .stages
16188 .get_mut(&api_id)
16189 .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16190 let stage = stages
16191 .get_mut(&stage_name)
16192 .ok_or_else(|| format!("Stage {stage_name} not yet provisioned for api {api_id}"))?;
16193 if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
16194 stage.description = Some(s.to_string());
16195 }
16196 if let Some(s) = props.get("DeploymentId").and_then(|v| v.as_str()) {
16197 stage.deployment_id = Some(s.to_string());
16198 }
16199 if let Some(b) = props.get("AutoDeploy").and_then(|v| v.as_bool()) {
16200 stage.auto_deploy = b;
16201 }
16202 stage.last_updated_date = Some(Utc::now());
16203 Ok(ProvisionResult::new(stage_name.clone())
16204 .with("StageName", stage_name)
16205 .with("ApiId", api_id))
16206 }
16207
16208 fn update_apigwv2_deployment(
16211 &self,
16212 existing: &StackResource,
16213 resource: &ResourceDefinition,
16214 ) -> Result<ProvisionResult, String> {
16215 let props = &resource.properties;
16216 let api_id = props
16217 .get("ApiId")
16218 .and_then(|v| v.as_str())
16219 .ok_or("ApiId is required")?
16220 .to_string();
16221 let deployment_id = existing.physical_id.clone();
16222 let mut accounts = self.apigatewayv2_state.write();
16223 let state = accounts.get_or_create(&self.account_id);
16224 let deps = state
16225 .deployments
16226 .get_mut(&api_id)
16227 .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16228 let dep = deps.get_mut(&deployment_id).ok_or_else(|| {
16229 format!("Deployment {deployment_id} not yet provisioned for api {api_id}")
16230 })?;
16231 if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
16232 dep.description = Some(s.to_string());
16233 }
16234 Ok(ProvisionResult::new(deployment_id.clone())
16235 .with("DeploymentId", deployment_id)
16236 .with("ApiId", api_id))
16237 }
16238
16239 fn update_apigwv2_authorizer(
16242 &self,
16243 existing: &StackResource,
16244 resource: &ResourceDefinition,
16245 ) -> Result<ProvisionResult, String> {
16246 let props = &resource.properties;
16247 let api_id = props
16248 .get("ApiId")
16249 .and_then(|v| v.as_str())
16250 .ok_or("ApiId is required")?
16251 .to_string();
16252 let authorizer_id = existing.physical_id.clone();
16253
16254 let mut accounts = self.apigatewayv2_state.write();
16255 let state = accounts.get_or_create(&self.account_id);
16256 let auths = state
16257 .authorizers
16258 .get_mut(&api_id)
16259 .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16260 let auth = auths.get_mut(&authorizer_id).ok_or_else(|| {
16261 format!("Authorizer {authorizer_id} not yet provisioned for api {api_id}")
16262 })?;
16263 if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
16264 auth.name = s.to_string();
16265 }
16266 if let Some(s) = props.get("AuthorizerType").and_then(|v| v.as_str()) {
16267 auth.authorizer_type = s.to_string();
16268 }
16269 if let Some(s) = props.get("AuthorizerUri").and_then(|v| v.as_str()) {
16270 auth.authorizer_uri = Some(s.to_string());
16271 }
16272 if let Some(arr) = props.get("IdentitySource").and_then(|v| v.as_array()) {
16273 auth.identity_source = Some(
16274 arr.iter()
16275 .filter_map(|v| v.as_str().map(String::from))
16276 .collect(),
16277 );
16278 }
16279 if let Some(obj) = props.get("JwtConfiguration").and_then(|v| v.as_object()) {
16280 auth.jwt_configuration = Some(ApiGwV2JwtConfiguration {
16281 audience: obj.get("Audience").and_then(|v| v.as_array()).map(|a| {
16282 a.iter()
16283 .filter_map(|v| v.as_str().map(String::from))
16284 .collect()
16285 }),
16286 issuer: obj.get("Issuer").and_then(|v| v.as_str()).map(String::from),
16287 });
16288 }
16289 Ok(ProvisionResult::new(authorizer_id.clone())
16290 .with("AuthorizerId", authorizer_id)
16291 .with("ApiId", api_id))
16292 }
16293
16294 fn update_apigwv2_domain_name(
16298 &self,
16299 existing: &StackResource,
16300 resource: &ResourceDefinition,
16301 ) -> Result<ProvisionResult, String> {
16302 let props = &resource.properties;
16303 let domain_name = existing.physical_id.clone();
16304 let body = serde_json::json!({
16305 "domainName": domain_name,
16306 "domainNameConfigurations": props.get("DomainNameConfigurations").cloned().unwrap_or(serde_json::json!([])),
16307 "mutualTlsAuthentication": props.get("MutualTlsAuthentication").cloned(),
16308 "apiMappingSelectionExpression": null,
16309 });
16310 let mut accounts = self.apigatewayv2_state.write();
16311 let state = accounts.get_or_create(&self.account_id);
16312 if !state.domain_names.contains_key(&domain_name) {
16313 return Err(format!("DomainName {domain_name} no longer exists"));
16314 }
16315 state.domain_names.insert(domain_name.clone(), body);
16316 Ok(ProvisionResult::new(domain_name.clone()).with("DomainName", domain_name))
16317 }
16318
16319 fn update_apigwv2_api_mapping(
16322 &self,
16323 existing: &StackResource,
16324 resource: &ResourceDefinition,
16325 ) -> Result<ProvisionResult, String> {
16326 let props = &resource.properties;
16327 let domain_name = props
16328 .get("DomainName")
16329 .and_then(|v| v.as_str())
16330 .ok_or("DomainName is required")?
16331 .to_string();
16332 let api_id = props
16333 .get("ApiId")
16334 .and_then(|v| v.as_str())
16335 .ok_or("ApiId is required")?
16336 .to_string();
16337 let stage = props
16338 .get("Stage")
16339 .and_then(|v| v.as_str())
16340 .ok_or("Stage is required")?
16341 .to_string();
16342 let api_mapping_key = props
16343 .get("ApiMappingKey")
16344 .and_then(|v| v.as_str())
16345 .map(String::from);
16346 let id = existing.physical_id.clone();
16347 let body = serde_json::json!({
16348 "apiMappingId": id,
16349 "apiId": api_id,
16350 "stage": stage,
16351 "apiMappingKey": api_mapping_key,
16352 });
16353 let mut accounts = self.apigatewayv2_state.write();
16354 let state = accounts.get_or_create(&self.account_id);
16355 let map = state
16356 .api_mappings
16357 .get_mut(&domain_name)
16358 .ok_or_else(|| format!("DomainName {domain_name} no longer exists"))?;
16359 map.insert(id.clone(), body);
16360 Ok(ProvisionResult::new(id.clone())
16361 .with("ApiMappingId", id)
16362 .with("DomainName", domain_name))
16363 }
16364
16365 fn update_apigwv2_vpc_link(
16369 &self,
16370 existing: &StackResource,
16371 resource: &ResourceDefinition,
16372 ) -> Result<ProvisionResult, String> {
16373 let props = &resource.properties;
16374 let id = existing.physical_id.clone();
16375 let mut accounts = self.apigatewayv2_state.write();
16376 let state = accounts.get_or_create(&self.account_id);
16377 let body = state
16378 .vpc_links
16379 .get_mut(&id)
16380 .ok_or_else(|| format!("VpcLink {id} no longer exists"))?;
16381 if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
16382 body["name"] = serde_json::Value::String(s.to_string());
16383 }
16384 if let Some(v) = props.get("SubnetIds").cloned() {
16385 body["subnetIds"] = v;
16386 }
16387 if let Some(v) = props.get("SecurityGroupIds").cloned() {
16388 body["securityGroupIds"] = v;
16389 }
16390 if let Some(v) = props.get("Tags").cloned() {
16391 body["tags"] = v;
16392 }
16393 Ok(ProvisionResult::new(id.clone()).with("VpcLinkId", id))
16394 }
16395
16396 fn update_apigwv2_model(
16400 &self,
16401 existing: &StackResource,
16402 resource: &ResourceDefinition,
16403 ) -> Result<ProvisionResult, String> {
16404 let props = &resource.properties;
16405 let api_id = props
16406 .get("ApiId")
16407 .and_then(|v| v.as_str())
16408 .ok_or("ApiId is required")?
16409 .to_string();
16410 let id = existing.physical_id.clone();
16411 let mut accounts = self.apigatewayv2_state.write();
16412 let state = accounts.get_or_create(&self.account_id);
16413 let map = state
16414 .models
16415 .get_mut(&api_id)
16416 .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16417 let body = map
16418 .get_mut(&id)
16419 .ok_or_else(|| format!("Model {id} not yet provisioned for api {api_id}"))?;
16420 if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
16421 body["name"] = serde_json::Value::String(s.to_string());
16422 }
16423 if let Some(s) = props.get("ContentType").and_then(|v| v.as_str()) {
16424 body["contentType"] = serde_json::Value::String(s.to_string());
16425 }
16426 if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
16427 body["description"] = serde_json::Value::String(s.to_string());
16428 }
16429 if let Some(v) = props.get("Schema") {
16430 body["schema"] = serde_json::Value::String(if let Some(s) = v.as_str() {
16431 s.to_string()
16432 } else {
16433 v.to_string()
16434 });
16435 }
16436 Ok(ProvisionResult::new(id.clone())
16437 .with("ModelId", id)
16438 .with("ApiId", api_id))
16439 }
16440
16441 fn create_ses_configuration_set(
16444 &self,
16445 resource: &ResourceDefinition,
16446 ) -> Result<ProvisionResult, String> {
16447 let props = &resource.properties;
16448 let name = props
16449 .get("Name")
16450 .and_then(|v| v.as_str())
16451 .map(String::from)
16452 .unwrap_or_else(|| format!("cfn-cs-{}", resource.logical_id));
16453 let sending_enabled = props
16454 .get("SendingOptions")
16455 .and_then(|v| v.get("SendingEnabled"))
16456 .and_then(|v| v.as_bool())
16457 .unwrap_or(true);
16458 let tls_policy = props
16459 .get("DeliveryOptions")
16460 .and_then(|v| v.get("TlsPolicy"))
16461 .and_then(|v| v.as_str())
16462 .unwrap_or("OPTIONAL")
16463 .to_string();
16464 let sending_pool_name = props
16465 .get("DeliveryOptions")
16466 .and_then(|v| v.get("SendingPoolName"))
16467 .and_then(|v| v.as_str())
16468 .map(String::from);
16469 let custom_redirect_domain = props
16470 .get("TrackingOptions")
16471 .and_then(|v| v.get("CustomRedirectDomain"))
16472 .and_then(|v| v.as_str())
16473 .map(String::from);
16474 let suppressed_reasons: Vec<String> = props
16475 .get("SuppressionOptions")
16476 .and_then(|v| v.get("SuppressedReasons"))
16477 .and_then(|v| v.as_array())
16478 .map(|arr| {
16479 arr.iter()
16480 .filter_map(|v| v.as_str().map(String::from))
16481 .collect()
16482 })
16483 .unwrap_or_default();
16484 let reputation_metrics_enabled = props
16485 .get("ReputationOptions")
16486 .and_then(|v| v.get("ReputationMetricsEnabled"))
16487 .and_then(|v| v.as_bool())
16488 .unwrap_or(false);
16489
16490 let cs = SesConfigurationSet {
16491 name: name.clone(),
16492 sending_enabled,
16493 tls_policy,
16494 sending_pool_name,
16495 custom_redirect_domain,
16496 https_policy: props
16497 .get("TrackingOptions")
16498 .and_then(|v| v.get("HttpsPolicy"))
16499 .and_then(|v| v.as_str())
16500 .map(String::from),
16501 suppressed_reasons,
16502 reputation_metrics_enabled,
16503 vdm_options: props.get("VdmOptions").cloned(),
16504 archive_arn: props
16505 .get("ArchivingOptions")
16506 .and_then(|v| v.get("ArchiveArn"))
16507 .and_then(|v| v.as_str())
16508 .map(String::from),
16509 archiving_options_present: props.get("ArchivingOptions").is_some_and(|v| v.is_object()),
16510 };
16511 let mut accounts = self.ses_state.write();
16512 let state = accounts.get_or_create(&self.account_id);
16513 state.configuration_sets.insert(name.clone(), cs);
16514 Ok(ProvisionResult::new(name.clone()).with("Name", name))
16515 }
16516
16517 fn delete_ses_configuration_set(&self, physical_id: &str) -> Result<(), String> {
16518 let mut accounts = self.ses_state.write();
16519 let state = accounts.get_or_create(&self.account_id);
16520 state.configuration_sets.remove(physical_id);
16521 state.event_destinations.remove(physical_id);
16522 Ok(())
16523 }
16524
16525 fn create_ses_event_destination(
16526 &self,
16527 resource: &ResourceDefinition,
16528 ) -> Result<ProvisionResult, String> {
16529 let props = &resource.properties;
16530 let cs_name = props
16531 .get("ConfigurationSetName")
16532 .and_then(|v| v.as_str())
16533 .ok_or("ConfigurationSetName is required")?
16534 .to_string();
16535 let dest_props = props
16536 .get("EventDestination")
16537 .and_then(|v| v.as_object())
16538 .ok_or("EventDestination is required")?;
16539 let name = dest_props
16540 .get("Name")
16541 .and_then(|v| v.as_str())
16542 .map(String::from)
16543 .unwrap_or_else(|| format!("cfn-ed-{}", resource.logical_id));
16544 let enabled = dest_props
16545 .get("Enabled")
16546 .and_then(|v| v.as_bool())
16547 .unwrap_or(true);
16548 let matching_event_types: Vec<String> = dest_props
16549 .get("MatchingEventTypes")
16550 .and_then(|v| v.as_array())
16551 .map(|arr| {
16552 arr.iter()
16553 .filter_map(|v| v.as_str().map(String::from))
16554 .collect()
16555 })
16556 .unwrap_or_default();
16557 let dest = SesEventDestination {
16558 name: name.clone(),
16559 enabled,
16560 matching_event_types,
16561 kinesis_firehose_destination: dest_props.get("KinesisFirehoseDestination").cloned(),
16562 cloud_watch_destination: dest_props.get("CloudWatchDestination").cloned(),
16563 sns_destination: dest_props.get("SnsDestination").cloned(),
16564 event_bridge_destination: dest_props.get("EventBridgeDestination").cloned(),
16565 pinpoint_destination: dest_props.get("PinpointDestination").cloned(),
16566 };
16567 let mut accounts = self.ses_state.write();
16568 let state = accounts.get_or_create(&self.account_id);
16569 if !state.configuration_sets.contains_key(&cs_name) {
16570 return Err(format!("ConfigurationSet {cs_name} not yet provisioned"));
16571 }
16572 let dests = state.event_destinations.entry(cs_name.clone()).or_default();
16573 dests.retain(|d| d.name != name);
16574 dests.push(dest);
16575 let physical = format!("{cs_name}|{name}");
16576 Ok(ProvisionResult::new(physical)
16577 .with("Name", name)
16578 .with("ConfigurationSetName", cs_name))
16579 }
16580
16581 fn delete_ses_event_destination(
16582 &self,
16583 physical_id: &str,
16584 _attributes: &BTreeMap<String, String>,
16585 ) -> Result<(), String> {
16586 let mut parts = physical_id.splitn(2, '|');
16587 let Some(cs) = parts.next() else {
16588 return Ok(());
16589 };
16590 let Some(name) = parts.next() else {
16591 return Ok(());
16592 };
16593 let mut accounts = self.ses_state.write();
16594 let state = accounts.get_or_create(&self.account_id);
16595 if let Some(dests) = state.event_destinations.get_mut(cs) {
16596 dests.retain(|d| d.name != name);
16597 }
16598 Ok(())
16599 }
16600
16601 fn create_ses_email_identity(
16602 &self,
16603 resource: &ResourceDefinition,
16604 ) -> Result<ProvisionResult, String> {
16605 let props = &resource.properties;
16606 let identity_name = props
16607 .get("EmailIdentity")
16608 .and_then(|v| v.as_str())
16609 .ok_or("EmailIdentity is required")?
16610 .to_string();
16611 let identity_type = if identity_name.contains('@') {
16612 "EMAIL_ADDRESS"
16613 } else {
16614 "DOMAIN"
16615 }
16616 .to_string();
16617 let dkim_signing_enabled = props
16618 .get("DkimAttributes")
16619 .and_then(|v| v.get("SigningEnabled"))
16620 .and_then(|v| v.as_bool())
16621 .unwrap_or(true);
16622 let dkim_signing_attributes_origin = props
16623 .get("DkimSigningAttributes")
16624 .map(|_| "EXTERNAL")
16625 .unwrap_or("AWS_SES")
16626 .to_string();
16627 let mail_from_domain = props
16628 .get("MailFromAttributes")
16629 .and_then(|v| v.get("MailFromDomain"))
16630 .and_then(|v| v.as_str())
16631 .map(String::from);
16632 let mail_from_behavior = props
16633 .get("MailFromAttributes")
16634 .and_then(|v| v.get("BehaviorOnMxFailure"))
16635 .and_then(|v| v.as_str())
16636 .unwrap_or("USE_DEFAULT_VALUE")
16637 .to_string();
16638 let configuration_set_name = props
16639 .get("ConfigurationSetAttributes")
16640 .and_then(|v| v.get("ConfigurationSetName"))
16641 .and_then(|v| v.as_str())
16642 .map(String::from);
16643 let email_forwarding_enabled = props
16644 .get("FeedbackAttributes")
16645 .and_then(|v| v.get("EmailForwardingEnabled"))
16646 .and_then(|v| v.as_bool())
16647 .unwrap_or(true);
16648
16649 let identity = SesEmailIdentity {
16650 identity_name: identity_name.clone(),
16651 identity_type,
16652 verified: true,
16653 created_at: Utc::now(),
16654 dkim_signing_enabled,
16655 dkim_signing_attributes_origin,
16656 dkim_domain_signing_private_key: None,
16657 dkim_domain_signing_selector: None,
16658 dkim_next_signing_key_length: None,
16659 dkim_public_key_b64: None,
16660 email_forwarding_enabled,
16661 mail_from_domain,
16662 mail_from_behavior_on_mx_failure: mail_from_behavior,
16663 mail_from_domain_status: "Success".to_string(),
16664 configuration_set_name,
16665 };
16666 let mut accounts = self.ses_state.write();
16667 let state = accounts.get_or_create(&self.account_id);
16668 state.identities.insert(identity_name.clone(), identity);
16669 Ok(ProvisionResult::new(identity_name.clone()).with("EmailIdentity", identity_name))
16670 }
16671
16672 fn delete_ses_email_identity(&self, physical_id: &str) -> Result<(), String> {
16673 let mut accounts = self.ses_state.write();
16674 let state = accounts.get_or_create(&self.account_id);
16675 state.identities.remove(physical_id);
16676 Ok(())
16677 }
16678
16679 fn create_ses_template(
16680 &self,
16681 resource: &ResourceDefinition,
16682 ) -> Result<ProvisionResult, String> {
16683 let props = &resource.properties;
16684 let template_block = props
16685 .get("Template")
16686 .and_then(|v| v.as_object())
16687 .ok_or("Template is required")?;
16688 let template_name = template_block
16689 .get("TemplateName")
16690 .and_then(|v| v.as_str())
16691 .map(String::from)
16692 .unwrap_or_else(|| format!("cfn-tpl-{}", resource.logical_id));
16693 let tpl = SesEmailTemplate {
16694 template_name: template_name.clone(),
16695 subject: template_block
16696 .get("SubjectPart")
16697 .and_then(|v| v.as_str())
16698 .map(String::from),
16699 html_body: template_block
16700 .get("HtmlPart")
16701 .and_then(|v| v.as_str())
16702 .map(String::from),
16703 text_body: template_block
16704 .get("TextPart")
16705 .and_then(|v| v.as_str())
16706 .map(String::from),
16707 created_at: Utc::now(),
16708 };
16709 let mut accounts = self.ses_state.write();
16710 let state = accounts.get_or_create(&self.account_id);
16711 state.templates.insert(template_name.clone(), tpl);
16712 Ok(ProvisionResult::new(template_name.clone()).with("TemplateName", template_name))
16713 }
16714
16715 fn delete_ses_template(&self, physical_id: &str) -> Result<(), String> {
16716 let mut accounts = self.ses_state.write();
16717 let state = accounts.get_or_create(&self.account_id);
16718 state.templates.remove(physical_id);
16719 Ok(())
16720 }
16721
16722 fn create_ses_contact_list(
16723 &self,
16724 resource: &ResourceDefinition,
16725 ) -> Result<ProvisionResult, String> {
16726 let props = &resource.properties;
16727 let name = props
16728 .get("ContactListName")
16729 .and_then(|v| v.as_str())
16730 .map(String::from)
16731 .unwrap_or_else(|| format!("cfn-cl-{}", resource.logical_id));
16732 let description = props
16733 .get("Description")
16734 .and_then(|v| v.as_str())
16735 .map(String::from);
16736 let now = Utc::now();
16737 let cl = SesContactList {
16738 contact_list_name: name.clone(),
16739 description,
16740 topics: Vec::new(),
16741 created_at: now,
16742 last_updated_at: now,
16743 };
16744 let mut accounts = self.ses_state.write();
16745 let state = accounts.get_or_create(&self.account_id);
16746 state.contact_lists.insert(name.clone(), cl);
16747 Ok(ProvisionResult::new(name.clone()).with("ContactListName", name))
16748 }
16749
16750 fn delete_ses_contact_list(&self, physical_id: &str) -> Result<(), String> {
16751 let mut accounts = self.ses_state.write();
16752 let state = accounts.get_or_create(&self.account_id);
16753 state.contact_lists.remove(physical_id);
16754 state.contacts.remove(physical_id);
16755 Ok(())
16756 }
16757
16758 fn create_ses_dedicated_ip_pool(
16759 &self,
16760 resource: &ResourceDefinition,
16761 ) -> Result<ProvisionResult, String> {
16762 let props = &resource.properties;
16763 let name = props
16764 .get("PoolName")
16765 .and_then(|v| v.as_str())
16766 .map(String::from)
16767 .unwrap_or_else(|| format!("cfn-pool-{}", resource.logical_id));
16768 let scaling_mode = props
16769 .get("ScalingMode")
16770 .and_then(|v| v.as_str())
16771 .unwrap_or("STANDARD")
16772 .to_string();
16773 let pool = SesDedicatedIpPool {
16774 pool_name: name.clone(),
16775 scaling_mode,
16776 };
16777 let mut accounts = self.ses_state.write();
16778 let state = accounts.get_or_create(&self.account_id);
16779 state.dedicated_ip_pools.insert(name.clone(), pool);
16780 Ok(ProvisionResult::new(name.clone()).with("PoolName", name))
16781 }
16782
16783 fn delete_ses_dedicated_ip_pool(&self, physical_id: &str) -> Result<(), String> {
16784 let mut accounts = self.ses_state.write();
16785 let state = accounts.get_or_create(&self.account_id);
16786 state.dedicated_ip_pools.remove(physical_id);
16787 Ok(())
16788 }
16789
16790 fn create_ses_receipt_rule_set(
16791 &self,
16792 resource: &ResourceDefinition,
16793 ) -> Result<ProvisionResult, String> {
16794 let props = &resource.properties;
16795 let name = props
16796 .get("RuleSetName")
16797 .and_then(|v| v.as_str())
16798 .map(String::from)
16799 .unwrap_or_else(|| format!("cfn-rs-{}", resource.logical_id));
16800 let rs = SesReceiptRuleSet {
16801 name: name.clone(),
16802 rules: Vec::new(),
16803 created_at: Utc::now(),
16804 };
16805 let mut accounts = self.ses_state.write();
16806 let state = accounts.get_or_create(&self.account_id);
16807 state.receipt_rule_sets.insert(name.clone(), rs);
16808 Ok(ProvisionResult::new(name.clone()).with("RuleSetName", name))
16809 }
16810
16811 fn delete_ses_receipt_rule_set(&self, physical_id: &str) -> Result<(), String> {
16812 let mut accounts = self.ses_state.write();
16813 let state = accounts.get_or_create(&self.account_id);
16814 state.receipt_rule_sets.remove(physical_id);
16815 Ok(())
16816 }
16817
16818 fn create_ses_receipt_rule(
16819 &self,
16820 resource: &ResourceDefinition,
16821 ) -> Result<ProvisionResult, String> {
16822 let props = &resource.properties;
16823 let rule_set_name = props
16824 .get("RuleSetName")
16825 .and_then(|v| v.as_str())
16826 .ok_or("RuleSetName is required")?
16827 .to_string();
16828 let rule_block = props
16829 .get("Rule")
16830 .and_then(|v| v.as_object())
16831 .ok_or("Rule is required")?;
16832 let name = rule_block
16833 .get("Name")
16834 .and_then(|v| v.as_str())
16835 .map(String::from)
16836 .unwrap_or_else(|| format!("cfn-rule-{}", resource.logical_id));
16837 let enabled = rule_block
16838 .get("Enabled")
16839 .and_then(|v| v.as_bool())
16840 .unwrap_or(true);
16841 let scan_enabled = rule_block
16842 .get("ScanEnabled")
16843 .and_then(|v| v.as_bool())
16844 .unwrap_or(false);
16845 let tls_policy = rule_block
16846 .get("TlsPolicy")
16847 .and_then(|v| v.as_str())
16848 .unwrap_or("Optional")
16849 .to_string();
16850 let recipients: Vec<String> = rule_block
16851 .get("Recipients")
16852 .and_then(|v| v.as_array())
16853 .map(|arr| {
16854 arr.iter()
16855 .filter_map(|v| v.as_str().map(String::from))
16856 .collect()
16857 })
16858 .unwrap_or_default();
16859 let actions: Vec<SesReceiptAction> = rule_block
16860 .get("Actions")
16861 .and_then(|v| v.as_array())
16862 .map(|arr| arr.iter().filter_map(parse_ses_receipt_action).collect())
16863 .unwrap_or_default();
16864
16865 let rule = SesReceiptRule {
16866 name: name.clone(),
16867 enabled,
16868 scan_enabled,
16869 tls_policy,
16870 recipients,
16871 actions,
16872 };
16873 let mut accounts = self.ses_state.write();
16874 let state = accounts.get_or_create(&self.account_id);
16875 let rs = state
16876 .receipt_rule_sets
16877 .get_mut(&rule_set_name)
16878 .ok_or_else(|| format!("ReceiptRuleSet {rule_set_name} not yet provisioned"))?;
16879 rs.rules.retain(|r| r.name != name);
16880 rs.rules.push(rule);
16881 let physical = format!("{rule_set_name}|{name}");
16882 Ok(ProvisionResult::new(physical)
16883 .with("Name", name)
16884 .with("RuleSetName", rule_set_name))
16885 }
16886
16887 fn delete_ses_receipt_rule(
16888 &self,
16889 physical_id: &str,
16890 _attributes: &BTreeMap<String, String>,
16891 ) -> Result<(), String> {
16892 let mut parts = physical_id.splitn(2, '|');
16893 let Some(rs_name) = parts.next() else {
16894 return Ok(());
16895 };
16896 let Some(rule_name) = parts.next() else {
16897 return Ok(());
16898 };
16899 let mut accounts = self.ses_state.write();
16900 let state = accounts.get_or_create(&self.account_id);
16901 if let Some(rs) = state.receipt_rule_sets.get_mut(rs_name) {
16902 rs.rules.retain(|r| r.name != rule_name);
16903 }
16904 Ok(())
16905 }
16906
16907 fn create_ses_receipt_filter(
16908 &self,
16909 resource: &ResourceDefinition,
16910 ) -> Result<ProvisionResult, String> {
16911 let props = &resource.properties;
16912 let filter_block = props
16913 .get("Filter")
16914 .and_then(|v| v.as_object())
16915 .ok_or("Filter is required")?;
16916 let name = filter_block
16917 .get("Name")
16918 .and_then(|v| v.as_str())
16919 .map(String::from)
16920 .unwrap_or_else(|| format!("cfn-filter-{}", resource.logical_id));
16921 let ip_block = filter_block
16922 .get("IpFilter")
16923 .and_then(|v| v.as_object())
16924 .ok_or("Filter.IpFilter is required")?;
16925 let cidr = ip_block
16926 .get("Cidr")
16927 .and_then(|v| v.as_str())
16928 .ok_or("Filter.IpFilter.Cidr is required")?
16929 .to_string();
16930 let policy = ip_block
16931 .get("Policy")
16932 .and_then(|v| v.as_str())
16933 .unwrap_or("Block")
16934 .to_string();
16935 let filter = SesReceiptFilter {
16936 name: name.clone(),
16937 ip_filter: SesIpFilter { cidr, policy },
16938 };
16939 let mut accounts = self.ses_state.write();
16940 let state = accounts.get_or_create(&self.account_id);
16941 state.receipt_filters.insert(name.clone(), filter);
16942 Ok(ProvisionResult::new(name.clone()).with("Name", name))
16943 }
16944
16945 fn delete_ses_receipt_filter(&self, physical_id: &str) -> Result<(), String> {
16946 let mut accounts = self.ses_state.write();
16947 let state = accounts.get_or_create(&self.account_id);
16948 state.receipt_filters.remove(physical_id);
16949 Ok(())
16950 }
16951
16952 fn create_ses_vdm_attributes(
16953 &self,
16954 resource: &ResourceDefinition,
16955 ) -> Result<ProvisionResult, String> {
16956 let props = &resource.properties;
16957 let mut accounts = self.ses_state.write();
16958 let state = accounts.get_or_create(&self.account_id);
16959 state.account_settings.vdm_attributes = Some(props.clone());
16960 Ok(ProvisionResult::new(format!("vdm-{}", resource.logical_id)))
16961 }
16962
16963 fn create_secrets_manager_rotation_schedule(
16966 &self,
16967 resource: &ResourceDefinition,
16968 ) -> Result<ProvisionResult, String> {
16969 let props = &resource.properties;
16970 let secret_id = props
16971 .get("SecretId")
16972 .and_then(|v| v.as_str())
16973 .ok_or("SecretId is required")?
16974 .to_string();
16975 let rotation_lambda_arn = props
16976 .get("RotationLambdaARN")
16977 .and_then(|v| v.as_str())
16978 .map(String::from);
16979 let automatically_after_days = props
16980 .get("RotationRules")
16981 .and_then(|v| v.get("AutomaticallyAfterDays"))
16982 .and_then(|v| v.as_i64());
16983 let mut accounts = self.secretsmanager_state.write();
16984 let state = accounts.get_or_create(&self.account_id);
16985 let secret_arn = if state.secrets.contains_key(&secret_id) {
16986 secret_id.clone()
16987 } else {
16988 let candidate = format!(
16989 "arn:aws:secretsmanager:{}:{}:secret:{}",
16990 state.region, state.account_id, secret_id
16991 );
16992 if state.secrets.contains_key(&candidate) {
16993 candidate
16994 } else {
16995 return Err(format!("Secret {secret_id} not yet provisioned"));
16996 }
16997 };
16998 let secret = state
16999 .secrets
17000 .get_mut(&secret_arn)
17001 .ok_or_else(|| format!("Secret {secret_arn} not found"))?;
17002 secret.rotation_enabled = Some(true);
17003 secret.rotation_lambda_arn = rotation_lambda_arn;
17004 secret.rotation_rules = Some(RotationRules {
17005 automatically_after_days,
17006 });
17007 secret.last_changed_at = Utc::now();
17008 Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
17009 }
17010
17011 fn delete_secrets_manager_rotation_schedule(&self, physical_id: &str) -> Result<(), String> {
17012 let mut accounts = self.secretsmanager_state.write();
17013 let state = accounts.get_or_create(&self.account_id);
17014 if let Some(secret) = state.secrets.get_mut(physical_id) {
17015 secret.rotation_enabled = Some(false);
17016 secret.rotation_lambda_arn = None;
17017 secret.rotation_rules = None;
17018 secret.last_changed_at = Utc::now();
17019 }
17020 Ok(())
17021 }
17022
17023 fn create_secrets_manager_resource_policy(
17024 &self,
17025 resource: &ResourceDefinition,
17026 ) -> Result<ProvisionResult, String> {
17027 let props = &resource.properties;
17028 let secret_id = props
17029 .get("SecretId")
17030 .and_then(|v| v.as_str())
17031 .ok_or("SecretId is required")?
17032 .to_string();
17033 let policy_doc = props
17034 .get("ResourcePolicy")
17035 .ok_or("ResourcePolicy is required")?;
17036 let policy_str = match policy_doc {
17037 serde_json::Value::String(s) => s.clone(),
17038 other => other.to_string(),
17039 };
17040 let mut accounts = self.secretsmanager_state.write();
17041 let state = accounts.get_or_create(&self.account_id);
17042 let secret_arn = if state.secrets.contains_key(&secret_id) {
17043 secret_id.clone()
17044 } else {
17045 let candidate = format!(
17046 "arn:aws:secretsmanager:{}:{}:secret:{}",
17047 state.region, state.account_id, secret_id
17048 );
17049 if state.secrets.contains_key(&candidate) {
17050 candidate
17051 } else {
17052 return Err(format!("Secret {secret_id} not yet provisioned"));
17053 }
17054 };
17055 let secret = state
17056 .secrets
17057 .get_mut(&secret_arn)
17058 .ok_or_else(|| format!("Secret {secret_arn} not found"))?;
17059 secret.resource_policy = Some(policy_str);
17060 secret.last_changed_at = Utc::now();
17061 Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
17062 }
17063
17064 fn delete_secrets_manager_resource_policy(&self, physical_id: &str) -> Result<(), String> {
17065 let mut accounts = self.secretsmanager_state.write();
17066 let state = accounts.get_or_create(&self.account_id);
17067 if let Some(secret) = state.secrets.get_mut(physical_id) {
17068 secret.resource_policy = None;
17069 secret.last_changed_at = Utc::now();
17070 }
17071 Ok(())
17072 }
17073
17074 fn create_secrets_manager_target_attachment(
17075 &self,
17076 resource: &ResourceDefinition,
17077 ) -> Result<ProvisionResult, String> {
17078 let props = &resource.properties;
17079 let secret_id = props
17080 .get("SecretId")
17081 .and_then(|v| v.as_str())
17082 .ok_or("SecretId is required")?
17083 .to_string();
17084 let target_type = props
17085 .get("TargetType")
17086 .and_then(|v| v.as_str())
17087 .ok_or("TargetType is required")?;
17088 let target_id = props
17089 .get("TargetId")
17090 .and_then(|v| v.as_str())
17091 .ok_or("TargetId is required")?;
17092 let mut accounts = self.secretsmanager_state.write();
17093 let state = accounts.get_or_create(&self.account_id);
17094 let secret_arn = if state.secrets.contains_key(&secret_id) {
17095 secret_id.clone()
17096 } else {
17097 let candidate = format!(
17098 "arn:aws:secretsmanager:{}:{}:secret:{}",
17099 state.region, state.account_id, secret_id
17100 );
17101 if state.secrets.contains_key(&candidate) {
17102 candidate
17103 } else {
17104 return Err(format!("Secret {secret_id} not yet provisioned"));
17105 }
17106 };
17107 let secret = state
17108 .secrets
17109 .get_mut(&secret_arn)
17110 .ok_or_else(|| format!("Secret {secret_arn} not found"))?;
17111 let now = Utc::now();
17117 if secret.current_version_id.is_none() {
17118 let version_id = Uuid::new_v4().to_string();
17119 secret.versions.insert(
17120 version_id.clone(),
17121 SecretVersion {
17122 version_id: version_id.clone(),
17123 secret_string: Some("{}".to_string()),
17124 secret_binary: None,
17125 stages: vec!["AWSCURRENT".to_string()],
17126 created_at: now,
17127 },
17128 );
17129 secret.current_version_id = Some(version_id);
17130 }
17131 if let Some(version_id) = secret.current_version_id.clone() {
17132 if let Some(version) = secret.versions.get_mut(&version_id) {
17133 let mut existing: serde_json::Value = version
17134 .secret_string
17135 .as_deref()
17136 .and_then(|s| serde_json::from_str(s).ok())
17137 .unwrap_or_else(|| serde_json::json!({}));
17138 if let Some(obj) = existing.as_object_mut() {
17139 let engine = match target_type {
17140 "AWS::RDS::DBInstance" | "AWS::RDS::DBCluster" => "postgres",
17141 _ => "unknown",
17142 };
17143 obj.entry("engine".to_string())
17144 .or_insert(serde_json::json!(engine));
17145 obj.insert("host".to_string(), serde_json::json!(target_id));
17146 obj.entry("dbInstanceIdentifier".to_string())
17147 .or_insert(serde_json::json!(target_id));
17148 }
17149 version.secret_string = Some(existing.to_string());
17150 }
17151 }
17152 secret.last_changed_at = now;
17153 Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
17154 }
17155
17156 fn create_athena_work_group(
17159 &self,
17160 resource: &ResourceDefinition,
17161 ) -> Result<ProvisionResult, String> {
17162 let props = &resource.properties;
17163 let name = props
17164 .get("Name")
17165 .and_then(|v| v.as_str())
17166 .unwrap_or(&resource.logical_id)
17167 .to_string();
17168 let description = props
17169 .get("Description")
17170 .and_then(|v| v.as_str())
17171 .map(|s| s.to_string());
17172 let configuration = props.get("Configuration").cloned();
17173 let state_str = props
17174 .get("State")
17175 .and_then(|v| v.as_str())
17176 .unwrap_or("ENABLED");
17177 let tags = Self::parse_athena_tags(props.get("Tags"));
17178
17179 let mut accounts = self.athena_state.write();
17180 let account = accounts
17181 .accounts
17182 .entry(self.account_id.clone())
17183 .or_default();
17184 account.ensure_initialized();
17185 if account.work_groups.contains_key(&name) {
17186 return Err(format!("WorkGroup {name} already exists"));
17187 }
17188 let wg = WorkGroup {
17189 name: name.clone(),
17190 state: state_str.to_string(),
17191 description,
17192 configuration,
17193 creation_time: Utc::now(),
17194 engine_version: Some("AUTO".to_string()),
17195 };
17196 let arn = format!(
17197 "arn:aws:athena:{}:{}:workgroup/{}",
17198 self.region, self.account_id, name
17199 );
17200 account.work_groups.insert(name.clone(), wg);
17201 if !tags.is_empty() {
17202 account.tags.insert(arn.clone(), tags);
17203 }
17204 Ok(ProvisionResult::new(name.clone())
17205 .with("Arn", arn)
17206 .with("Name", name))
17207 }
17208
17209 fn delete_athena_work_group(&self, physical_id: &str) -> Result<(), 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 account.work_groups.remove(physical_id);
17216 Ok(())
17217 }
17218
17219 fn get_att_athena_work_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
17220 let mut accounts = self.athena_state.write();
17221 let account = accounts
17222 .accounts
17223 .entry(self.account_id.clone())
17224 .or_default();
17225 let wg = account.work_groups.get(physical_id)?;
17226 match attribute {
17227 "Arn" => Some(format!(
17228 "arn:aws:athena:{}:{}:workgroup/{}",
17229 self.region, self.account_id, wg.name
17230 )),
17231 "Name" => Some(wg.name.clone()),
17232 _ => None,
17233 }
17234 }
17235
17236 fn create_athena_data_catalog(
17237 &self,
17238 resource: &ResourceDefinition,
17239 ) -> Result<ProvisionResult, String> {
17240 let props = &resource.properties;
17241 let name = props
17242 .get("Name")
17243 .and_then(|v| v.as_str())
17244 .unwrap_or(&resource.logical_id)
17245 .to_string();
17246 let cat_type = props
17247 .get("Type")
17248 .and_then(|v| v.as_str())
17249 .ok_or("Type is required")?
17250 .to_string();
17251 let description = props
17252 .get("Description")
17253 .and_then(|v| v.as_str())
17254 .map(|s| s.to_string());
17255 let parameters = props
17256 .get("Parameters")
17257 .and_then(|v| v.as_object())
17258 .map(|m| {
17259 m.iter()
17260 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
17261 .collect()
17262 })
17263 .unwrap_or_default();
17264 let connection_type = props
17265 .get("ConnectionType")
17266 .and_then(|v| v.as_str())
17267 .map(|s| s.to_string());
17268 let tags = Self::parse_athena_tags(props.get("Tags"));
17269
17270 let mut accounts = self.athena_state.write();
17271 let account = accounts
17272 .accounts
17273 .entry(self.account_id.clone())
17274 .or_default();
17275 account.ensure_initialized();
17276 if account.data_catalogs.contains_key(&name) {
17277 return Err(format!("DataCatalog {name} already exists"));
17278 }
17279 let cat = DataCatalog {
17280 name: name.clone(),
17281 description,
17282 cat_type,
17283 parameters,
17284 status: "CREATE_COMPLETE".to_string(),
17285 connection_type,
17286 error: None,
17287 };
17288 let arn = format!(
17289 "arn:aws:athena:{}:{}:datacatalog/{}",
17290 self.region, self.account_id, name
17291 );
17292 account.data_catalogs.insert(name.clone(), cat);
17293 if !tags.is_empty() {
17294 account.tags.insert(arn.clone(), tags);
17295 }
17296 Ok(ProvisionResult::new(name.clone())
17297 .with("Arn", arn)
17298 .with("Name", name))
17299 }
17300
17301 fn delete_athena_data_catalog(&self, physical_id: &str) -> Result<(), 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 account.data_catalogs.remove(physical_id);
17308 Ok(())
17309 }
17310
17311 fn get_att_athena_data_catalog(&self, physical_id: &str, attribute: &str) -> Option<String> {
17312 let mut accounts = self.athena_state.write();
17313 let account = accounts
17314 .accounts
17315 .entry(self.account_id.clone())
17316 .or_default();
17317 let cat = account.data_catalogs.get(physical_id)?;
17318 match attribute {
17319 "Arn" => Some(format!(
17320 "arn:aws:athena:{}:{}:datacatalog/{}",
17321 self.region, self.account_id, cat.name
17322 )),
17323 "Name" => Some(cat.name.clone()),
17324 _ => None,
17325 }
17326 }
17327
17328 fn create_athena_named_query(
17329 &self,
17330 resource: &ResourceDefinition,
17331 ) -> Result<ProvisionResult, String> {
17332 let props = &resource.properties;
17333 let name = props
17334 .get("Name")
17335 .and_then(|v| v.as_str())
17336 .ok_or("Name is required")?
17337 .to_string();
17338 let database = props
17339 .get("Database")
17340 .and_then(|v| v.as_str())
17341 .ok_or("Database is required")?
17342 .to_string();
17343 let query_string = props
17344 .get("QueryString")
17345 .and_then(|v| v.as_str())
17346 .ok_or("QueryString is required")?
17347 .to_string();
17348 let description = props
17349 .get("Description")
17350 .and_then(|v| v.as_str())
17351 .map(|s| s.to_string());
17352 let work_group = props
17353 .get("WorkGroup")
17354 .and_then(|v| v.as_str())
17355 .unwrap_or("primary")
17356 .to_string();
17357
17358 let mut accounts = self.athena_state.write();
17359 let account = accounts
17360 .accounts
17361 .entry(self.account_id.clone())
17362 .or_default();
17363 account.ensure_initialized();
17364 if !account.work_groups.contains_key(&work_group) {
17365 return Err(format!("Workgroup {work_group} not found"));
17366 }
17367 let id = Uuid::new_v4().to_string();
17368 let nq = NamedQuery {
17369 named_query_id: id.clone(),
17370 name,
17371 description,
17372 database,
17373 query_string,
17374 work_group,
17375 last_used_at: None,
17376 };
17377 account.named_queries.insert(id.clone(), nq);
17378 Ok(ProvisionResult::new(id.clone()).with("NamedQueryId", id))
17379 }
17380
17381 fn delete_athena_named_query(&self, physical_id: &str) -> Result<(), 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 account.named_queries.remove(physical_id);
17388 Ok(())
17389 }
17390
17391 fn get_att_athena_named_query(&self, physical_id: &str, attribute: &str) -> Option<String> {
17392 let mut accounts = self.athena_state.write();
17393 let account = accounts
17394 .accounts
17395 .entry(self.account_id.clone())
17396 .or_default();
17397 let nq = account.named_queries.get(physical_id)?;
17398 match attribute {
17399 "NamedQueryId" => Some(nq.named_query_id.clone()),
17400 _ => None,
17401 }
17402 }
17403
17404 fn create_athena_prepared_statement(
17405 &self,
17406 resource: &ResourceDefinition,
17407 ) -> Result<ProvisionResult, String> {
17408 let props = &resource.properties;
17409 let statement_name = props
17410 .get("StatementName")
17411 .and_then(|v| v.as_str())
17412 .ok_or("StatementName is required")?
17413 .to_string();
17414 let work_group_name = props
17415 .get("WorkGroupName")
17416 .and_then(|v| v.as_str())
17417 .ok_or("WorkGroupName is required")?
17418 .to_string();
17419 let query_statement = props
17420 .get("QueryStatement")
17421 .and_then(|v| v.as_str())
17422 .ok_or("QueryStatement is required")?
17423 .to_string();
17424 let description = props
17425 .get("Description")
17426 .and_then(|v| v.as_str())
17427 .map(|s| s.to_string());
17428
17429 let mut accounts = self.athena_state.write();
17430 let account = accounts
17431 .accounts
17432 .entry(self.account_id.clone())
17433 .or_default();
17434 account.ensure_initialized();
17435 if !account.work_groups.contains_key(&work_group_name) {
17436 return Err(format!("Workgroup {work_group_name} not found"));
17437 }
17438 let key = (work_group_name.clone(), statement_name.clone());
17439 if account.prepared_statements.contains_key(&key) {
17440 return Err(format!(
17441 "PreparedStatement {statement_name} already exists in {work_group_name}"
17442 ));
17443 }
17444 let ps = PreparedStatement {
17445 statement_name: statement_name.clone(),
17446 work_group_name: work_group_name.clone(),
17447 query_statement,
17448 description,
17449 last_modified_time: Utc::now(),
17450 };
17451 let physical_id = format!("{work_group_name}|{statement_name}");
17452 account.prepared_statements.insert(key, ps);
17453 Ok(ProvisionResult::new(physical_id))
17454 }
17455
17456 fn delete_athena_prepared_statement(
17457 &self,
17458 physical_id: &str,
17459 _attrs: &BTreeMap<String, String>,
17460 ) -> Result<(), String> {
17461 let mut accounts = self.athena_state.write();
17462 let account = accounts
17463 .accounts
17464 .entry(self.account_id.clone())
17465 .or_default();
17466 let parts: Vec<&str> = physical_id.split('|').collect();
17467 if parts.len() != 2 {
17468 return Err(format!(
17469 "Invalid PreparedStatement physical id: {physical_id}"
17470 ));
17471 }
17472 let key = (parts[0].to_string(), parts[1].to_string());
17473 account.prepared_statements.remove(&key);
17474 Ok(())
17475 }
17476
17477 fn get_att_athena_prepared_statement(
17478 &self,
17479 physical_id: &str,
17480 attribute: &str,
17481 ) -> Option<String> {
17482 let mut accounts = self.athena_state.write();
17483 let account = accounts
17484 .accounts
17485 .entry(self.account_id.clone())
17486 .or_default();
17487 let parts: Vec<&str> = physical_id.split('|').collect();
17488 if parts.len() != 2 {
17489 return None;
17490 }
17491 let ps = account
17492 .prepared_statements
17493 .get(&(parts[0].to_string(), parts[1].to_string()))?;
17494 match attribute {
17495 "StatementName" => Some(ps.statement_name.clone()),
17496 "WorkGroupName" => Some(ps.work_group_name.clone()),
17497 _ => None,
17498 }
17499 }
17500
17501 fn parse_athena_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
17502 let mut out = BTreeMap::new();
17503 let Some(arr) = value.and_then(|v| v.as_array()) else {
17504 return out;
17505 };
17506 for tag in arr {
17507 if let (Some(k), Some(v)) = (
17508 tag.get("Key").and_then(|v| v.as_str()),
17509 tag.get("Value").and_then(|v| v.as_str()),
17510 ) {
17511 out.insert(k.to_string(), v.to_string());
17512 }
17513 }
17514 out
17515 }
17516
17517 fn create_glue_database(
17518 &self,
17519 resource: &ResourceDefinition,
17520 ) -> Result<ProvisionResult, String> {
17521 let props = &resource.properties;
17522 let input = props
17523 .get("DatabaseInput")
17524 .ok_or("DatabaseInput is required")?;
17525 let name = input
17526 .get("Name")
17527 .and_then(|v| v.as_str())
17528 .unwrap_or(&resource.logical_id)
17529 .to_string();
17530 let description = input
17531 .get("Description")
17532 .and_then(|v| v.as_str())
17533 .map(|s| s.to_string());
17534 let location_uri = input
17535 .get("LocationUri")
17536 .and_then(|v| v.as_str())
17537 .map(|s| s.to_string());
17538 let parameters = input
17539 .get("Parameters")
17540 .and_then(|v| v.as_object())
17541 .map(|m| {
17542 m.iter()
17543 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
17544 .collect()
17545 })
17546 .unwrap_or_default();
17547
17548 let mut accounts = self.glue_state.write();
17549 let state = accounts.get_or_create(&self.account_id, &self.region);
17550 let dbs = state.dbs_in_mut(&self.region);
17551 if dbs.contains_key(&name) {
17552 return Err(format!("Database {name} already exists"));
17553 }
17554 dbs.insert(
17555 name.clone(),
17556 fakecloud_glue::Database {
17557 name: name.clone(),
17558 description,
17559 location_uri,
17560 parameters,
17561 created_at: Utc::now(),
17562 catalog_id: self.account_id.clone(),
17563 tables: BTreeMap::new(),
17564 },
17565 );
17566 Ok(ProvisionResult::new(name.clone()))
17567 }
17568
17569 fn delete_glue_database(&self, physical_id: &str) -> Result<(), String> {
17570 let mut accounts = self.glue_state.write();
17571 let state = accounts.get_or_create(&self.account_id, &self.region);
17572 state.dbs_in_mut(&self.region).remove(physical_id);
17573 Ok(())
17574 }
17575
17576 fn create_glue_table(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
17577 let props = &resource.properties;
17578 let db_name = props
17579 .get("DatabaseName")
17580 .and_then(|v| v.as_str())
17581 .ok_or("DatabaseName is required")?
17582 .to_string();
17583 let input = props.get("TableInput").ok_or("TableInput is required")?;
17584 let name = input
17585 .get("Name")
17586 .and_then(|v| v.as_str())
17587 .ok_or("TableInput.Name is required")?
17588 .to_string();
17589 let now = Utc::now();
17590
17591 let mut accounts = self.glue_state.write();
17592 let state = accounts.get_or_create(&self.account_id, &self.region);
17593 let dbs = state.dbs_in_mut(&self.region);
17594 let db = dbs
17595 .get_mut(&db_name)
17596 .ok_or_else(|| format!("Database {db_name} not found"))?;
17597 if db.tables.contains_key(&name) {
17598 return Err(format!("Table {name} already exists"));
17599 }
17600 db.tables.insert(
17601 name.clone(),
17602 fakecloud_glue::Table {
17603 name: name.clone(),
17604 database_name: db_name.clone(),
17605 description: input
17606 .get("Description")
17607 .and_then(|v| v.as_str())
17608 .map(|s| s.to_string()),
17609 owner: input
17610 .get("Owner")
17611 .and_then(|v| v.as_str())
17612 .map(|s| s.to_string()),
17613 create_time: now,
17614 update_time: now,
17615 last_access_time: None,
17616 retention: input.get("Retention").and_then(|v| v.as_i64()).unwrap_or(0),
17617 storage_descriptor: None,
17618 partition_keys: Vec::new(),
17619 view_original_text: input
17620 .get("ViewOriginalText")
17621 .and_then(|v| v.as_str())
17622 .map(|s| s.to_string()),
17623 view_expanded_text: input
17624 .get("ViewExpandedText")
17625 .and_then(|v| v.as_str())
17626 .map(|s| s.to_string()),
17627 table_type: input
17628 .get("TableType")
17629 .and_then(|v| v.as_str())
17630 .map(|s| s.to_string()),
17631 parameters: BTreeMap::new(),
17632 partitions: BTreeMap::new(),
17633 },
17634 );
17635 let physical_id = format!("{db_name}|{name}");
17636 Ok(ProvisionResult::new(physical_id))
17637 }
17638
17639 fn delete_glue_table(&self, physical_id: &str) -> Result<(), String> {
17640 let mut accounts = self.glue_state.write();
17641 let state = accounts.get_or_create(&self.account_id, &self.region);
17642 let parts: Vec<&str> = physical_id.split('|').collect();
17643 if parts.len() != 2 {
17644 return Err(format!("Invalid Glue table physical id: {physical_id}"));
17645 }
17646 let dbs = state.dbs_in_mut(&self.region);
17647 let db = dbs
17648 .get_mut(parts[0])
17649 .ok_or_else(|| format!("Database {} not found", parts[0]))?;
17650 db.tables.remove(parts[1]);
17651 Ok(())
17652 }
17653
17654 fn create_glue_partition(
17655 &self,
17656 resource: &ResourceDefinition,
17657 ) -> Result<ProvisionResult, String> {
17658 let props = &resource.properties;
17659 let db_name = props
17660 .get("DatabaseName")
17661 .and_then(|v| v.as_str())
17662 .ok_or("DatabaseName is required")?
17663 .to_string();
17664 let table_name = props
17665 .get("TableName")
17666 .and_then(|v| v.as_str())
17667 .ok_or("TableName is required")?
17668 .to_string();
17669 let input = props
17670 .get("PartitionInput")
17671 .ok_or("PartitionInput is required")?;
17672 let values: Vec<String> = input
17673 .get("Values")
17674 .and_then(|v| v.as_array())
17675 .map(|arr| {
17676 arr.iter()
17677 .filter_map(|v| v.as_str().map(|s| s.to_string()))
17678 .collect()
17679 })
17680 .unwrap_or_default();
17681 if values.is_empty() {
17682 return Err("PartitionInput.Values is required".to_string());
17683 }
17684 let key = values
17685 .iter()
17686 .map(|v| {
17687 v.replace('%', "%25")
17688 .replace('|', "%7C")
17689 .replace('/', "%2F")
17690 })
17691 .collect::<Vec<_>>()
17692 .join("/");
17693
17694 let mut accounts = self.glue_state.write();
17695 let state = accounts.get_or_create(&self.account_id, &self.region);
17696 let dbs = state.dbs_in_mut(&self.region);
17697 let db = dbs
17698 .get_mut(&db_name)
17699 .ok_or_else(|| format!("Database {db_name} not found"))?;
17700 let table = db
17701 .tables
17702 .get_mut(&table_name)
17703 .ok_or_else(|| format!("Table {table_name} not found"))?;
17704 if table.partitions.contains_key(&key) {
17705 return Err(format!("Partition {key} already exists"));
17706 }
17707 table.partitions.insert(
17708 key.clone(),
17709 fakecloud_glue::Partition {
17710 values: values.clone(),
17711 database_name: db_name.clone(),
17712 table_name: table_name.clone(),
17713 create_time: Utc::now(),
17714 last_access_time: None,
17715 storage_descriptor: None,
17716 parameters: BTreeMap::new(),
17717 },
17718 );
17719 let physical_id = format!("{db_name}|{table_name}|{key}");
17720 Ok(ProvisionResult::new(physical_id))
17721 }
17722
17723 fn delete_glue_partition(
17724 &self,
17725 physical_id: &str,
17726 _attrs: &BTreeMap<String, String>,
17727 ) -> Result<(), String> {
17728 let mut accounts = self.glue_state.write();
17729 let state = accounts.get_or_create(&self.account_id, &self.region);
17730 let parts: Vec<&str> = physical_id.split('|').collect();
17731 if parts.len() != 3 {
17732 return Err(format!("Invalid Glue partition physical id: {physical_id}"));
17733 }
17734 let dbs = state.dbs_in_mut(&self.region);
17735 let db = dbs
17736 .get_mut(parts[0])
17737 .ok_or_else(|| format!("Database {} not found", parts[0]))?;
17738 let table = db
17739 .tables
17740 .get_mut(parts[1])
17741 .ok_or_else(|| format!("Table {} not found", parts[1]))?;
17742 table.partitions.remove(parts[2]);
17743 Ok(())
17744 }
17745
17746 fn create_cloudformation_stack(
17749 &self,
17750 resource: &ResourceDefinition,
17751 ) -> Result<ProvisionResult, String> {
17752 let props = &resource.properties;
17753
17754 let template_body = if let Some(body) = props.get("TemplateBody").and_then(|v| v.as_str()) {
17755 body.to_string()
17756 } else if let Some(url) = props.get("TemplateURL").and_then(|v| v.as_str()) {
17757 self.fetch_template_from_url(url)?
17758 } else {
17759 return Err(
17760 "AWS::CloudFormation::Stack requires TemplateURL or TemplateBody".to_string(),
17761 );
17762 };
17763
17764 let child_parameters =
17765 if let Some(params) = props.get("Parameters").and_then(|v| v.as_object()) {
17766 let mut map = BTreeMap::new();
17767 for (k, v) in params {
17768 if let Some(s) = v.as_str() {
17769 map.insert(k.clone(), s.to_string());
17770 } else {
17771 map.insert(k.clone(), v.to_string());
17772 }
17773 }
17774 map
17775 } else {
17776 BTreeMap::new()
17777 };
17778
17779 let child_tags = if let Some(tags) = props.get("Tags").and_then(|v| v.as_object()) {
17780 let mut map = BTreeMap::new();
17781 for (k, v) in tags {
17782 if let Some(s) = v.as_str() {
17783 map.insert(k.clone(), s.to_string());
17784 }
17785 }
17786 map
17787 } else {
17788 BTreeMap::new()
17789 };
17790
17791 let parsed = crate::template::parse_template(&template_body, &child_parameters)
17792 .map_err(|e| format!("Failed to parse nested template: {e}"))?;
17793
17794 let child_stack_name = format!(
17795 "{}-Nested-{}",
17796 resource.logical_id,
17797 std::time::SystemTime::now()
17798 .duration_since(std::time::UNIX_EPOCH)
17799 .map(|d| d.as_nanos())
17800 .unwrap_or(0)
17801 );
17802 let child_stack_id = format!(
17803 "arn:aws:cloudformation:{}:{}:stack/{}/{}",
17804 self.region,
17805 self.account_id,
17806 child_stack_name,
17807 Uuid::new_v4()
17808 );
17809
17810 let child_provisioner = ResourceProvisioner {
17811 sqs_state: self.sqs_state.clone(),
17812 sns_state: self.sns_state.clone(),
17813 ssm_state: self.ssm_state.clone(),
17814 iam_state: self.iam_state.clone(),
17815 s3_state: self.s3_state.clone(),
17816 eventbridge_state: self.eventbridge_state.clone(),
17817 dynamodb_state: self.dynamodb_state.clone(),
17818 logs_state: self.logs_state.clone(),
17819 lambda_state: self.lambda_state.clone(),
17820 secretsmanager_state: self.secretsmanager_state.clone(),
17821 kinesis_state: self.kinesis_state.clone(),
17822 kms_state: self.kms_state.clone(),
17823 ecr_state: self.ecr_state.clone(),
17824 cloudwatch_state: self.cloudwatch_state.clone(),
17825 elbv2_state: self.elbv2_state.clone(),
17826 organizations_state: self.organizations_state.clone(),
17827 cognito_state: self.cognito_state.clone(),
17828 rds_state: self.rds_state.clone(),
17829 ecs_state: self.ecs_state.clone(),
17830 acm_state: self.acm_state.clone(),
17831 elasticache_state: self.elasticache_state.clone(),
17832 route53_state: self.route53_state.clone(),
17833 cloudfront_state: self.cloudfront_state.clone(),
17834 stepfunctions_state: self.stepfunctions_state.clone(),
17835 wafv2_state: self.wafv2_state.clone(),
17836 apigateway_state: self.apigateway_state.clone(),
17837 apigatewayv2_state: self.apigatewayv2_state.clone(),
17838 ses_state: self.ses_state.clone(),
17839 app_autoscaling_state: self.app_autoscaling_state.clone(),
17840 athena_state: self.athena_state.clone(),
17841 firehose_state: self.firehose_state.clone(),
17842 glue_state: self.glue_state.clone(),
17843 cloudformation_state: self.cloudformation_state.clone(),
17844 delivery: self.delivery.clone(),
17845 account_id: self.account_id.clone(),
17846 region: self.region.clone(),
17847 stack_id: child_stack_id.clone(),
17848 };
17849
17850 let child_resources = crate::service::provision_stack_resources(
17851 &child_provisioner,
17852 &parsed.resources,
17853 &template_body,
17854 &child_parameters,
17855 )
17856 .map_err(|e| format!("Failed to provision nested stack: {e}"))?;
17857
17858 let child_outputs = parsed.outputs;
17859
17860 let stack = crate::state::Stack {
17861 name: child_stack_name.clone(),
17862 stack_id: child_stack_id.clone(),
17863 template: template_body.clone(),
17864 status: "CREATE_COMPLETE".to_string(),
17865 resources: child_resources.clone(),
17866 parameters: child_parameters,
17867 tags: child_tags,
17868 created_at: Utc::now(),
17869 updated_at: None,
17870 description: parsed.description,
17871 notification_arns: Vec::new(),
17872 outputs: child_outputs
17873 .iter()
17874 .map(|o| crate::state::StackOutput {
17875 key: o.logical_id.clone(),
17876 value: o.value.clone(),
17877 description: o.description.clone(),
17878 export_name: o.export_name.clone(),
17879 })
17880 .collect(),
17881 };
17882
17883 {
17884 let mut accounts = self.cloudformation_state.write();
17885 let state = accounts.get_or_create(&self.account_id);
17886 state.stacks.insert(child_stack_name.clone(), stack);
17887
17888 crate::service::record_stack_status_event(
17889 state,
17890 &child_stack_id,
17891 &child_stack_name,
17892 "AWS::CloudFormation::Stack",
17893 "CREATE_IN_PROGRESS",
17894 );
17895 let changes: Vec<crate::service::ResourceChange> = child_resources
17896 .iter()
17897 .map(|r| crate::service::ResourceChange {
17898 action: crate::service::ResourceChangeAction::Create,
17899 logical_id: r.logical_id.clone(),
17900 physical_id: r.physical_id.clone(),
17901 resource_type: r.resource_type.clone(),
17902 })
17903 .collect();
17904 crate::service::record_stack_events(
17905 state,
17906 &child_stack_id,
17907 &child_stack_name,
17908 &changes,
17909 );
17910 crate::service::record_stack_status_event(
17911 state,
17912 &child_stack_id,
17913 &child_stack_name,
17914 "AWS::CloudFormation::Stack",
17915 "CREATE_COMPLETE",
17916 );
17917 }
17918
17919 let mut result = ProvisionResult::new(child_stack_id.clone());
17920 for output in &child_outputs {
17921 result.attributes.insert(
17922 format!("Outputs.{}", output.logical_id),
17923 output.value.clone(),
17924 );
17925 }
17926 Ok(result)
17927 }
17928
17929 fn delete_cloudformation_stack(&self, physical_id: &str) -> Result<(), String> {
17930 let stack = {
17931 let accounts = self.cloudformation_state.read();
17932 let state = accounts.get(&self.account_id);
17933 state.and_then(|s| {
17934 s.stacks
17935 .values()
17936 .find(|st| st.stack_id == physical_id)
17937 .cloned()
17938 })
17939 };
17940
17941 if let Some(stack) = stack {
17942 let stack_name = stack.name.clone();
17943 let stack_id = stack.stack_id.clone();
17944
17945 for resource in stack.resources.iter().rev() {
17946 let _ = self.delete_resource(resource);
17947 }
17948
17949 {
17950 let mut accounts = self.cloudformation_state.write();
17951 let state = accounts.get_or_create(&self.account_id);
17952 state.stacks.remove(&stack_name);
17953
17954 crate::service::record_stack_status_event(
17955 state,
17956 &stack_id,
17957 &stack_name,
17958 "AWS::CloudFormation::Stack",
17959 "DELETE_IN_PROGRESS",
17960 );
17961 crate::service::record_stack_status_event(
17962 state,
17963 &stack_id,
17964 &stack_name,
17965 "AWS::CloudFormation::Stack",
17966 "DELETE_COMPLETE",
17967 );
17968 }
17969 }
17970
17971 Ok(())
17972 }
17973
17974 fn get_att_cloudformation_stack(&self, physical_id: &str, attribute: &str) -> Option<String> {
17975 let accounts = self.cloudformation_state.read();
17976 let state = accounts.get(&self.account_id)?;
17977 let stack = state.stacks.values().find(|s| s.stack_id == physical_id)?;
17978
17979 if let Some(output_key) = attribute.strip_prefix("Outputs.") {
17980 return stack
17981 .outputs
17982 .iter()
17983 .find(|o| o.key == output_key)
17984 .map(|o| o.value.clone());
17985 }
17986
17987 match attribute {
17988 "Outputs" => Some(
17989 serde_json::to_string(
17990 &stack
17991 .outputs
17992 .iter()
17993 .map(|o| (o.key.clone(), o.value.clone()))
17994 .collect::<std::collections::BTreeMap<String, String>>(),
17995 )
17996 .unwrap_or_default(),
17997 ),
17998 _ => None,
17999 }
18000 }
18001
18002 fn fetch_template_from_url(&self, url: &str) -> Result<String, String> {
18003 if let Some(rest) = url.strip_prefix("s3://") {
18004 let parts: Vec<&str> = rest.splitn(2, '/').collect();
18005 if parts.len() != 2 {
18006 return Err("Invalid s3:// URL".to_string());
18007 }
18008 return self.fetch_s3_template(parts[0], parts[1]);
18009 }
18010
18011 if let Some(rest) = url.strip_prefix("https://s3.amazonaws.com/") {
18012 let parts: Vec<&str> = rest.splitn(2, '/').collect();
18013 if parts.len() != 2 {
18014 return Err("Invalid S3 HTTPS URL".to_string());
18015 }
18016 return self.fetch_s3_template(parts[0], parts[1]);
18017 }
18018
18019 if let Some(host_rest) = url.strip_prefix("https://") {
18020 if let Some(slash_pos) = host_rest.find('/') {
18021 let host = &host_rest[..slash_pos];
18022 let key = &host_rest[slash_pos + 1..];
18023 if let Some(bucket) = host.strip_suffix(".s3.amazonaws.com") {
18024 return self.fetch_s3_template(bucket, key);
18025 }
18026 if host.contains(".s3.") && host.ends_with(".amazonaws.com") {
18027 let bucket = host.split(".s3.").next().unwrap_or("");
18028 if !bucket.is_empty() {
18029 return self.fetch_s3_template(bucket, key);
18030 }
18031 }
18032 }
18033 }
18034
18035 Err(format!("Unsupported TemplateURL: {url}"))
18036 }
18037
18038 fn fetch_s3_template(&self, bucket: &str, key: &str) -> Result<String, String> {
18039 let mut s3_accounts = self.s3_state.write();
18040 let s3_state = s3_accounts.get_or_create(&self.account_id);
18041 let bucket_obj = s3_state
18042 .buckets
18043 .get(bucket)
18044 .ok_or_else(|| format!("S3 bucket not found: {bucket}"))?;
18045 let obj = bucket_obj
18046 .objects
18047 .get(key)
18048 .ok_or_else(|| format!("S3 object not found: {bucket}/{key}"))?;
18049 let bytes = s3_state
18050 .read_body(&obj.body)
18051 .map_err(|e| format!("Failed to read S3 object body: {e}"))?;
18052 String::from_utf8(bytes.to_vec()).map_err(|e| format!("S3 object is not valid UTF-8: {e}"))
18053 }
18054}
18055
18056fn generate_secret_string_payload(gen: &serde_json::Value) -> Result<String, String> {
18065 let length = gen
18066 .get("PasswordLength")
18067 .and_then(|v| v.as_i64())
18068 .unwrap_or(32) as usize;
18069 let exclude_lowercase = gen
18070 .get("ExcludeLowercase")
18071 .and_then(|v| v.as_bool())
18072 .unwrap_or(false);
18073 let exclude_uppercase = gen
18074 .get("ExcludeUppercase")
18075 .and_then(|v| v.as_bool())
18076 .unwrap_or(false);
18077 let exclude_numbers = gen
18078 .get("ExcludeNumbers")
18079 .and_then(|v| v.as_bool())
18080 .unwrap_or(false);
18081 let exclude_punctuation = gen
18082 .get("ExcludePunctuation")
18083 .and_then(|v| v.as_bool())
18084 .unwrap_or(false);
18085 let include_space = gen
18086 .get("IncludeSpace")
18087 .and_then(|v| v.as_bool())
18088 .unwrap_or(false);
18089 let exclude_chars = gen
18090 .get("ExcludeCharacters")
18091 .and_then(|v| v.as_str())
18092 .unwrap_or("")
18093 .to_string();
18094
18095 let lowercase = "abcdefghijklmnopqrstuvwxyz";
18096 let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
18097 let digits = "0123456789";
18098 let punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
18099
18100 let mut pool = String::new();
18101 if !exclude_lowercase {
18102 pool.extend(lowercase.chars().filter(|c| !exclude_chars.contains(*c)));
18103 }
18104 if !exclude_uppercase {
18105 pool.extend(uppercase.chars().filter(|c| !exclude_chars.contains(*c)));
18106 }
18107 if !exclude_numbers {
18108 pool.extend(digits.chars().filter(|c| !exclude_chars.contains(*c)));
18109 }
18110 if !exclude_punctuation {
18111 pool.extend(punctuation.chars().filter(|c| !exclude_chars.contains(*c)));
18112 }
18113 if include_space && !exclude_chars.contains(' ') {
18114 pool.push(' ');
18115 }
18116 if pool.is_empty() {
18117 return Err("GenerateSecretString character pool is empty".to_string());
18118 }
18119
18120 let pool_chars: Vec<char> = pool.chars().collect();
18121 let mut password = String::with_capacity(length);
18122 let mut counter: u64 = std::time::SystemTime::now()
18123 .duration_since(std::time::UNIX_EPOCH)
18124 .map(|d| d.as_nanos() as u64)
18125 .unwrap_or(0);
18126 while password.len() < length {
18127 counter = counter.wrapping_add(0x9E3779B97F4A7C15);
18130 let mut z = counter;
18131 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
18132 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
18133 z ^= z >> 31;
18134 let idx = (z as usize) % pool_chars.len();
18135 password.push(pool_chars[idx]);
18136 }
18137
18138 let template = gen.get("SecretStringTemplate").and_then(|v| v.as_str());
18139 let key = gen.get("GenerateStringKey").and_then(|v| v.as_str());
18140 match (template, key) {
18141 (Some(tmpl), Some(k)) => {
18142 let mut value: serde_json::Value = serde_json::from_str(tmpl)
18143 .map_err(|e| format!("SecretStringTemplate is not valid JSON: {e}"))?;
18144 if let Some(obj) = value.as_object_mut() {
18145 obj.insert(k.to_string(), serde_json::Value::String(password));
18146 Ok(value.to_string())
18147 } else {
18148 Err("SecretStringTemplate must be a JSON object".to_string())
18149 }
18150 }
18151 _ => Ok(password),
18152 }
18153}
18154
18155fn parse_ses_receipt_action(value: &serde_json::Value) -> Option<SesReceiptAction> {
18156 let obj = value.as_object()?;
18157 if let Some(s3) = obj.get("S3Action").and_then(|v| v.as_object()) {
18158 let bucket_name = s3.get("BucketName").and_then(|v| v.as_str())?.to_string();
18159 return Some(SesReceiptAction::S3 {
18160 bucket_name,
18161 object_key_prefix: s3
18162 .get("ObjectKeyPrefix")
18163 .and_then(|v| v.as_str())
18164 .map(String::from),
18165 topic_arn: s3
18166 .get("TopicArn")
18167 .and_then(|v| v.as_str())
18168 .map(String::from),
18169 kms_key_arn: s3
18170 .get("KmsKeyArn")
18171 .and_then(|v| v.as_str())
18172 .map(String::from),
18173 });
18174 }
18175 if let Some(sns) = obj.get("SNSAction").and_then(|v| v.as_object()) {
18176 return Some(SesReceiptAction::Sns {
18177 topic_arn: sns.get("TopicArn").and_then(|v| v.as_str())?.to_string(),
18178 encoding: sns
18179 .get("Encoding")
18180 .and_then(|v| v.as_str())
18181 .map(String::from),
18182 });
18183 }
18184 if let Some(la) = obj.get("LambdaAction").and_then(|v| v.as_object()) {
18185 return Some(SesReceiptAction::Lambda {
18186 function_arn: la.get("FunctionArn").and_then(|v| v.as_str())?.to_string(),
18187 invocation_type: la
18188 .get("InvocationType")
18189 .and_then(|v| v.as_str())
18190 .map(String::from),
18191 topic_arn: la
18192 .get("TopicArn")
18193 .and_then(|v| v.as_str())
18194 .map(String::from),
18195 });
18196 }
18197 if let Some(b) = obj.get("BounceAction").and_then(|v| v.as_object()) {
18198 return Some(SesReceiptAction::Bounce {
18199 smtp_reply_code: b
18200 .get("SmtpReplyCode")
18201 .and_then(|v| v.as_str())
18202 .unwrap_or("550")
18203 .to_string(),
18204 message: b
18205 .get("Message")
18206 .and_then(|v| v.as_str())
18207 .unwrap_or("")
18208 .to_string(),
18209 sender: b
18210 .get("Sender")
18211 .and_then(|v| v.as_str())
18212 .unwrap_or("")
18213 .to_string(),
18214 status_code: b
18215 .get("StatusCode")
18216 .and_then(|v| v.as_str())
18217 .map(String::from),
18218 topic_arn: b.get("TopicArn").and_then(|v| v.as_str()).map(String::from),
18219 });
18220 }
18221 if let Some(ah) = obj.get("AddHeaderAction").and_then(|v| v.as_object()) {
18222 return Some(SesReceiptAction::AddHeader {
18223 header_name: ah.get("HeaderName").and_then(|v| v.as_str())?.to_string(),
18224 header_value: ah.get("HeaderValue").and_then(|v| v.as_str())?.to_string(),
18225 });
18226 }
18227 if let Some(s) = obj.get("StopAction").and_then(|v| v.as_object()) {
18228 return Some(SesReceiptAction::Stop {
18229 scope: s
18230 .get("Scope")
18231 .and_then(|v| v.as_str())
18232 .unwrap_or("RuleSet")
18233 .to_string(),
18234 topic_arn: s.get("TopicArn").and_then(|v| v.as_str()).map(String::from),
18235 });
18236 }
18237 None
18238}
18239
18240fn make_apigwv2_id(n: usize) -> String {
18244 let s = uuid::Uuid::new_v4().simple().to_string();
18245 s[..n.min(s.len())].to_string()
18246}
18247
18248fn cfn_as_i64(v: &serde_json::Value) -> Option<i64> {
18258 if let Some(n) = v.as_i64() {
18259 return Some(n);
18260 }
18261 v.as_str().and_then(|s| s.parse::<i64>().ok())
18262}
18263
18264fn lowercase_first_keys(value: serde_json::Value) -> serde_json::Value {
18265 match value {
18266 serde_json::Value::Object(map) => {
18267 let mut out = serde_json::Map::new();
18268 for (k, v) in map {
18269 let new_key = if let Some(first) = k.chars().next() {
18270 let mut s = String::with_capacity(k.len());
18271 s.extend(first.to_lowercase());
18272 s.push_str(&k[first.len_utf8()..]);
18273 s
18274 } else {
18275 k
18276 };
18277 out.insert(new_key, lowercase_first_keys(v));
18278 }
18279 serde_json::Value::Object(out)
18280 }
18281 serde_json::Value::Array(arr) => {
18282 serde_json::Value::Array(arr.into_iter().map(lowercase_first_keys).collect())
18283 }
18284 other => other,
18285 }
18286}
18287
18288fn synth_acm_domain_validation(
18295 domain_name: &str,
18296 sans: &[String],
18297 validation_method: &str,
18298) -> Vec<AcmDomainValidation> {
18299 let mut all = vec![domain_name.to_string()];
18300 for s in sans {
18301 if !all.contains(s) {
18302 all.push(s.clone());
18303 }
18304 }
18305 all.into_iter()
18306 .map(|name| AcmDomainValidation {
18307 domain_name: name.clone(),
18308 validation_status: "SUCCESS".to_string(),
18309 validation_method: validation_method.to_string(),
18310 resource_record_name: Some(format!("_amzn-validations.{name}.")),
18311 resource_record_type: Some("CNAME".to_string()),
18312 resource_record_value: Some(format!("{}.acm-validations.aws.", Uuid::new_v4())),
18313 })
18314 .collect()
18315}
18316
18317fn parse_acm_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
18319 let mut out = BTreeMap::new();
18320 if let Some(arr) = value.and_then(|v| v.as_array()) {
18321 for t in arr {
18322 if let (Some(k), Some(v)) = (
18323 t.get("Key").and_then(|v| v.as_str()),
18324 t.get("Value").and_then(|v| v.as_str()),
18325 ) {
18326 out.insert(k.to_string(), v.to_string());
18327 }
18328 }
18329 }
18330 out
18331}
18332
18333fn parse_ecs_tags(value: Option<&serde_json::Value>) -> Vec<EcsTagEntry> {
18335 let Some(arr) = value.and_then(|v| v.as_array()) else {
18336 return Vec::new();
18337 };
18338 arr.iter()
18339 .filter_map(|t| {
18340 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
18341 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
18342 Some(EcsTagEntry { key, value })
18343 })
18344 .collect()
18345}
18346
18347fn parse_ecs_cluster_name(input: &str) -> String {
18350 if let Some(after) = input.split(":cluster/").nth(1) {
18351 return after.to_string();
18352 }
18353 input.to_string()
18354}
18355
18356fn parse_td_arn(input: &str) -> (String, i32) {
18360 let suffix = input.rsplit('/').next().unwrap_or(input);
18361 if let Some((family, rev)) = suffix.split_once(':') {
18362 if let Ok(revision) = rev.parse::<i32>() {
18363 return (family.to_string(), revision);
18364 }
18365 }
18366 (input.to_string(), 1)
18367}
18368
18369fn parse_service_arn(input: &str) -> Option<(String, String)> {
18372 let after = input.split(":service/").nth(1)?;
18373 let mut parts = after.splitn(2, '/');
18374 let cluster = parts.next()?.to_string();
18375 let service = parts.next()?.to_string();
18376 Some((cluster, service))
18377}
18378
18379fn parse_rds_tags(value: Option<&serde_json::Value>) -> Vec<RdsTag> {
18381 let Some(arr) = value.and_then(|v| v.as_array()) else {
18382 return Vec::new();
18383 };
18384 arr.iter()
18385 .filter_map(|t| {
18386 let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
18387 let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
18388 Some(RdsTag { key, value })
18389 })
18390 .collect()
18391}
18392
18393fn rds_extras_mut<'a>(
18397 state: &'a mut fakecloud_rds::RdsState,
18398 category: &str,
18399) -> &'a mut BTreeMap<String, serde_json::Value> {
18400 state.extras.entry(category.to_string()).or_default()
18401}
18402
18403fn parse_cognito_string_array(value: Option<&serde_json::Value>) -> Vec<String> {
18407 value
18408 .and_then(|v| v.as_array())
18409 .map(|arr| {
18410 arr.iter()
18411 .filter_map(|v| v.as_str().map(|s| s.to_string()))
18412 .collect()
18413 })
18414 .unwrap_or_default()
18415}
18416
18417fn parse_cognito_password_policy(value: Option<&serde_json::Value>) -> PasswordPolicy {
18418 let Some(inner) = value
18419 .and_then(|v| v.get("PasswordPolicy"))
18420 .and_then(|v| v.as_object())
18421 else {
18422 return PasswordPolicy::default();
18423 };
18424 let mut p = PasswordPolicy::default();
18425 if let Some(n) = inner.get("MinimumLength").and_then(|v| v.as_i64()) {
18426 p.minimum_length = n;
18427 }
18428 if let Some(b) = inner.get("RequireUppercase").and_then(|v| v.as_bool()) {
18429 p.require_uppercase = b;
18430 }
18431 if let Some(b) = inner.get("RequireLowercase").and_then(|v| v.as_bool()) {
18432 p.require_lowercase = b;
18433 }
18434 if let Some(b) = inner.get("RequireNumbers").and_then(|v| v.as_bool()) {
18435 p.require_numbers = b;
18436 }
18437 if let Some(b) = inner.get("RequireSymbols").and_then(|v| v.as_bool()) {
18438 p.require_symbols = b;
18439 }
18440 if let Some(n) = inner
18441 .get("TemporaryPasswordValidityDays")
18442 .and_then(|v| v.as_i64())
18443 {
18444 p.temporary_password_validity_days = n;
18445 }
18446 p
18447}
18448
18449fn parse_cognito_schema_attribute(value: &serde_json::Value) -> Option<SchemaAttribute> {
18450 let name = value.get("Name").and_then(|v| v.as_str())?.to_string();
18451 Some(SchemaAttribute {
18452 name,
18453 attribute_data_type: value
18454 .get("AttributeDataType")
18455 .and_then(|v| v.as_str())
18456 .unwrap_or("String")
18457 .to_string(),
18458 developer_only_attribute: value
18459 .get("DeveloperOnlyAttribute")
18460 .and_then(|v| v.as_bool())
18461 .unwrap_or(false),
18462 mutable: value
18463 .get("Mutable")
18464 .and_then(|v| v.as_bool())
18465 .unwrap_or(true),
18466 required: value
18467 .get("Required")
18468 .and_then(|v| v.as_bool())
18469 .unwrap_or(false),
18470 string_attribute_constraints: None,
18471 number_attribute_constraints: None,
18472 })
18473}
18474
18475fn parse_cognito_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
18476 let mut out = BTreeMap::new();
18477 if let Some(obj) = value.and_then(|v| v.as_object()) {
18478 for (k, v) in obj {
18479 if let Some(s) = v.as_str() {
18480 out.insert(k.clone(), s.to_string());
18481 }
18482 }
18483 }
18484 out
18485}
18486
18487fn parse_cognito_email_configuration(
18488 value: Option<&serde_json::Value>,
18489) -> Option<EmailConfiguration> {
18490 let inner = value?.as_object()?;
18491 Some(EmailConfiguration {
18492 source_arn: inner
18493 .get("SourceArn")
18494 .and_then(|v| v.as_str())
18495 .map(|s| s.to_string()),
18496 reply_to_email_address: inner
18497 .get("ReplyToEmailAddress")
18498 .and_then(|v| v.as_str())
18499 .map(|s| s.to_string()),
18500 email_sending_account: inner
18501 .get("EmailSendingAccount")
18502 .and_then(|v| v.as_str())
18503 .map(|s| s.to_string()),
18504 from_email_address: inner
18505 .get("From")
18506 .and_then(|v| v.as_str())
18507 .map(|s| s.to_string()),
18508 configuration_set: inner
18509 .get("ConfigurationSet")
18510 .and_then(|v| v.as_str())
18511 .map(|s| s.to_string()),
18512 })
18513}
18514
18515fn parse_cognito_sms_configuration(value: Option<&serde_json::Value>) -> Option<SmsConfiguration> {
18516 let inner = value?.as_object()?;
18517 Some(SmsConfiguration {
18518 sns_caller_arn: inner
18519 .get("SnsCallerArn")
18520 .and_then(|v| v.as_str())
18521 .map(|s| s.to_string()),
18522 external_id: inner
18523 .get("ExternalId")
18524 .and_then(|v| v.as_str())
18525 .map(|s| s.to_string()),
18526 sns_region: inner
18527 .get("SnsRegion")
18528 .and_then(|v| v.as_str())
18529 .map(|s| s.to_string()),
18530 })
18531}
18532
18533fn parse_cognito_admin_create_user_config(
18534 value: Option<&serde_json::Value>,
18535) -> Option<AdminCreateUserConfig> {
18536 let inner = value?.as_object()?;
18537 Some(AdminCreateUserConfig {
18538 allow_admin_create_user_only: inner
18539 .get("AllowAdminCreateUserOnly")
18540 .and_then(|v| v.as_bool()),
18541 invite_message_template: None,
18542 unused_account_validity_days: inner
18543 .get("UnusedAccountValidityDays")
18544 .and_then(|v| v.as_i64()),
18545 })
18546}
18547
18548fn parse_cognito_account_recovery(
18549 value: Option<&serde_json::Value>,
18550) -> Option<AccountRecoverySetting> {
18551 let arr = value?.get("RecoveryMechanisms")?.as_array()?;
18552 Some(AccountRecoverySetting {
18553 recovery_mechanisms: arr
18554 .iter()
18555 .filter_map(|m| {
18556 let name = m.get("Name").and_then(|v| v.as_str())?.to_string();
18557 let priority = m.get("Priority").and_then(|v| v.as_i64()).unwrap_or(1);
18558 Some(RecoveryOption { name, priority })
18559 })
18560 .collect(),
18561 })
18562}
18563
18564fn parse_firehose_s3_destination(value: &serde_json::Value) -> Result<S3Destination, String> {
18565 let role_arn = value
18566 .get("RoleARN")
18567 .and_then(|v| v.as_str())
18568 .ok_or("S3 destination requires RoleARN")?
18569 .to_string();
18570 let bucket_arn = value
18571 .get("BucketARN")
18572 .and_then(|v| v.as_str())
18573 .ok_or("S3 destination requires BucketARN")?
18574 .to_string();
18575 let prefix = value
18576 .get("Prefix")
18577 .and_then(|v| v.as_str())
18578 .map(|s| s.to_string());
18579 let error_output_prefix = value
18580 .get("ErrorOutputPrefix")
18581 .and_then(|v| v.as_str())
18582 .map(|s| s.to_string());
18583 let mut buffering_size_mb = None;
18584 let mut buffering_interval_seconds = None;
18585 if let Some(hints) = value.get("BufferingHints") {
18586 buffering_size_mb = hints.get("SizeInMBs").and_then(|v| v.as_i64());
18587 buffering_interval_seconds = hints.get("IntervalInSeconds").and_then(|v| v.as_i64());
18588 }
18589 let compression_format = value
18590 .get("CompressionFormat")
18591 .and_then(|v| v.as_str())
18592 .map(|s| s.to_string());
18593
18594 Ok(S3Destination {
18595 destination_id: "destination-1".to_string(),
18596 role_arn,
18597 bucket_arn,
18598 prefix,
18599 error_output_prefix,
18600 buffering_size_mb,
18601 buffering_interval_seconds,
18602 compression_format,
18603 })
18604}
18605
18606#[cfg(test)]
18607mod tests {
18608 use super::*;
18609 use parking_lot::RwLock;
18610
18611 fn make_provisioner() -> ResourceProvisioner {
18612 ResourceProvisioner {
18613 sqs_state: Arc::new(RwLock::new(
18614 fakecloud_core::multi_account::MultiAccountState::new(
18615 "123456789012",
18616 "us-east-1",
18617 "http://localhost:4566",
18618 ),
18619 )),
18620 sns_state: Arc::new(RwLock::new(
18621 fakecloud_core::multi_account::MultiAccountState::new(
18622 "123456789012",
18623 "us-east-1",
18624 "http://localhost:4566",
18625 ),
18626 )),
18627 ssm_state: Arc::new(RwLock::new(
18628 fakecloud_core::multi_account::MultiAccountState::new(
18629 "123456789012",
18630 "us-east-1",
18631 "http://localhost:4566",
18632 ),
18633 )),
18634 iam_state: Arc::new(RwLock::new(
18635 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "http://localhost:4566"),
18636 )),
18637 s3_state: Arc::new(RwLock::new(fakecloud_core::multi_account::MultiAccountState::new(
18638 "123456789012",
18639 "us-east-1", "",
18640 ))),
18641 eventbridge_state: Arc::new(RwLock::new(
18642 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18643 )),
18644 dynamodb_state: Arc::new(RwLock::new(fakecloud_core::multi_account::MultiAccountState::new(
18645 "123456789012",
18646 "us-east-1", "",
18647 ))),
18648 logs_state: Arc::new(RwLock::new(
18649 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18650 )),
18651 lambda_state: Arc::new(RwLock::new(
18652 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18653 )),
18654 secretsmanager_state: Arc::new(RwLock::new(
18655 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18656 )),
18657 kinesis_state: Arc::new(RwLock::new(
18658 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18659 )),
18660 kms_state: Arc::new(RwLock::new(
18661 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18662 )),
18663 ecr_state: Arc::new(RwLock::new(
18664 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18665 )),
18666 cloudwatch_state: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
18667 elbv2_state: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
18668 organizations_state: Arc::new(RwLock::new(None)),
18669 cognito_state: Arc::new(RwLock::new(
18670 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18671 )),
18672 rds_state: Arc::new(RwLock::new(
18673 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18674 )),
18675 ecs_state: Arc::new(RwLock::new(
18676 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18677 )),
18678 acm_state: Arc::new(RwLock::new(fakecloud_acm::AcmAccounts::new())),
18679 elasticache_state: Arc::new(RwLock::new(
18680 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18681 )),
18682 route53_state: Arc::new(RwLock::new(fakecloud_route53::Route53Accounts::new())),
18683 cloudfront_state: Arc::new(RwLock::new(
18684 fakecloud_cloudfront::CloudFrontAccounts::new(),
18685 )),
18686 cloudformation_state: Arc::new(RwLock::new(
18687 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18688 )),
18689 stepfunctions_state: Arc::new(RwLock::new(
18690 fakecloud_core::multi_account::MultiAccountState::new(
18691 "123456789012",
18692 "us-east-1",
18693 "",
18694 ),
18695 )),
18696 wafv2_state: Arc::new(RwLock::new(fakecloud_wafv2::Wafv2Accounts::default())),
18697 apigateway_state: Arc::new(RwLock::new(
18698 fakecloud_core::multi_account::MultiAccountState::new(
18699 "123456789012",
18700 "us-east-1",
18701 "",
18702 ),
18703 )),
18704 apigatewayv2_state: Arc::new(RwLock::new(
18705 fakecloud_core::multi_account::MultiAccountState::new(
18706 "123456789012",
18707 "us-east-1",
18708 "",
18709 ),
18710 )),
18711 ses_state: Arc::new(RwLock::new(
18712 fakecloud_core::multi_account::MultiAccountState::new(
18713 "123456789012",
18714 "us-east-1",
18715 "",
18716 ),
18717 )),
18718 app_autoscaling_state: Arc::new(parking_lot::RwLock::new(
18719 fakecloud_application_autoscaling::ApplicationAutoScalingAccounts::new(),
18720 )),
18721 athena_state: Arc::new(parking_lot::RwLock::new(
18722 fakecloud_athena::AthenaAccounts::new(),
18723 )),
18724 firehose_state: Arc::new(parking_lot::RwLock::new(
18725 fakecloud_firehose::FirehoseAccounts::new(),
18726 )),
18727 glue_state: Arc::new(parking_lot::RwLock::new(
18728 fakecloud_glue::GlueAccounts::new(),
18729 )),
18730 delivery: Arc::new(DeliveryBus::new()),
18731 account_id: "123456789012".to_string(),
18732 region: "us-east-1".to_string(),
18733 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test/00000000-0000-0000-0000-000000000000".to_string(),
18734 }
18735 }
18736
18737 fn make_resource(
18738 resource_type: &str,
18739 logical_id: &str,
18740 props: serde_json::Value,
18741 ) -> ResourceDefinition {
18742 ResourceDefinition {
18743 logical_id: logical_id.to_string(),
18744 resource_type: resource_type.to_string(),
18745 properties: props,
18746 }
18747 }
18748
18749 #[test]
18750 fn sns_subscription_rejects_nonexistent_topic() {
18751 let prov = make_provisioner();
18752 let resource = make_resource(
18753 "AWS::SNS::Subscription",
18754 "MySub",
18755 serde_json::json!({
18756 "TopicArn": "arn:aws:sns:us-east-1:123456789012:NonExistent",
18757 "Protocol": "sqs",
18758 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:my-queue"
18759 }),
18760 );
18761 let result = prov.create_resource(&resource);
18762 assert!(result.is_err());
18763 assert!(result.unwrap_err().contains("does not exist"));
18764 }
18765
18766 #[test]
18767 fn sns_subscription_succeeds_when_topic_exists() {
18768 let prov = make_provisioner();
18769 let topic = make_resource(
18771 "AWS::SNS::Topic",
18772 "MyTopic",
18773 serde_json::json!({ "TopicName": "my-topic" }),
18774 );
18775 let topic_result = prov.create_resource(&topic);
18776 assert!(topic_result.is_ok());
18777 let topic_arn = topic_result.unwrap().physical_id;
18778
18779 let sub = make_resource(
18781 "AWS::SNS::Subscription",
18782 "MySub",
18783 serde_json::json!({
18784 "TopicArn": topic_arn,
18785 "Protocol": "sqs",
18786 "Endpoint": "arn:aws:sqs:us-east-1:123456789012:my-queue"
18787 }),
18788 );
18789 let result = prov.create_resource(&sub);
18790 assert!(result.is_ok());
18791 }
18792
18793 #[test]
18794 fn eventbridge_rule_arn_default_bus_omits_bus_name() {
18795 let prov = make_provisioner();
18796 let resource = make_resource(
18797 "AWS::Events::Rule",
18798 "MyRule",
18799 serde_json::json!({
18800 "Name": "my-rule",
18801 "ScheduleExpression": "rate(1 hour)"
18802 }),
18803 );
18804 let result = prov.create_resource(&resource).unwrap();
18805 assert_eq!(
18807 result.physical_id,
18808 "arn:aws:events:us-east-1:123456789012:rule/my-rule"
18809 );
18810 assert!(!result.physical_id.contains("rule/default/"));
18811 }
18812
18813 #[test]
18814 fn eventbridge_rule_arn_custom_bus_includes_bus_name() {
18815 let prov = make_provisioner();
18816 {
18818 let mut eb_accounts = prov.eventbridge_state.write();
18819 let state = eb_accounts.default_mut();
18820 state.buses.insert(
18821 "custom-bus".to_string(),
18822 fakecloud_eventbridge::EventBus {
18823 name: "custom-bus".to_string(),
18824 arn: "arn:aws:events:us-east-1:123456789012:event-bus/custom-bus".to_string(),
18825 policy: None,
18826 creation_time: Utc::now(),
18827 last_modified_time: Utc::now(),
18828 description: None,
18829 kms_key_identifier: None,
18830 dead_letter_config: None,
18831 tags: std::collections::BTreeMap::new(),
18832 },
18833 );
18834 }
18835 let resource = make_resource(
18836 "AWS::Events::Rule",
18837 "MyRule",
18838 serde_json::json!({
18839 "Name": "my-rule",
18840 "EventBusName": "custom-bus",
18841 "ScheduleExpression": "rate(1 hour)"
18842 }),
18843 );
18844 let result = prov.create_resource(&resource).unwrap();
18845 assert_eq!(
18846 result.physical_id,
18847 "arn:aws:events:us-east-1:123456789012:rule/custom-bus/my-rule"
18848 );
18849 }
18850
18851 #[test]
18852 fn eventbridge_rule_rejects_nonexistent_bus() {
18853 let prov = make_provisioner();
18854 let resource = make_resource(
18855 "AWS::Events::Rule",
18856 "MyRule",
18857 serde_json::json!({
18858 "Name": "my-rule",
18859 "EventBusName": "nonexistent-bus",
18860 "ScheduleExpression": "rate(1 hour)"
18861 }),
18862 );
18863 let result = prov.create_resource(&resource);
18864 assert!(result.is_err());
18865 assert!(result.unwrap_err().contains("does not exist"));
18866 }
18867
18868 #[test]
18869 fn custom_resource_requires_service_token() {
18870 let prov = make_provisioner();
18871 let resource = make_resource(
18872 "Custom::MyResource",
18873 "MyCustom",
18874 serde_json::json!({
18875 "Foo": "bar"
18876 }),
18877 );
18878 let result = prov.create_resource(&resource);
18879 assert!(result.is_err());
18880 assert!(
18881 result.unwrap_err().contains("ServiceToken"),
18882 "Should require ServiceToken property"
18883 );
18884 }
18885
18886 #[test]
18887 fn custom_resource_succeeds_without_lambda_delivery() {
18888 let prov = make_provisioner();
18891 let resource = make_resource(
18892 "Custom::MyResource",
18893 "MyCustom",
18894 serde_json::json!({
18895 "ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:my-func",
18896 "Foo": "bar"
18897 }),
18898 );
18899 let result = prov.create_resource(&resource);
18900 assert!(result.is_ok());
18901 let sr = result.unwrap();
18902 assert_eq!(sr.logical_id, "MyCustom");
18903 assert_eq!(sr.resource_type, "Custom::MyResource");
18904 assert!(sr.physical_id.starts_with("MyCustom-"));
18905 }
18906
18907 #[test]
18908 fn cloudformation_custom_resource_type_succeeds() {
18909 let prov = make_provisioner();
18910 let resource = make_resource(
18911 "AWS::CloudFormation::CustomResource",
18912 "MyCustom2",
18913 serde_json::json!({
18914 "ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:my-func",
18915 "Key": "value"
18916 }),
18917 );
18918 let result = prov.create_resource(&resource);
18919 assert!(result.is_ok());
18920 let sr = result.unwrap();
18921 assert_eq!(sr.resource_type, "AWS::CloudFormation::CustomResource");
18922 }
18923
18924 #[test]
18927 fn sqs_queue_create_and_delete() {
18928 let prov = make_provisioner();
18929 let res = make_resource(
18930 "AWS::SQS::Queue",
18931 "MyQ",
18932 serde_json::json!({"QueueName": "my-q"}),
18933 );
18934 let sr = prov.create_resource(&res).unwrap();
18935 assert!(sr.physical_id.contains("my-q"));
18936 assert_eq!(sr.resource_type, "AWS::SQS::Queue");
18937 prov.delete_resource(&sr).unwrap();
18938 }
18939
18940 #[test]
18941 fn sqs_queue_fifo_with_suffix() {
18942 let prov = make_provisioner();
18943 let res = make_resource(
18944 "AWS::SQS::Queue",
18945 "FifoQ",
18946 serde_json::json!({"QueueName": "my-fifo.fifo", "FifoQueue": true}),
18947 );
18948 let sr = prov.create_resource(&res).unwrap();
18949 assert!(sr.physical_id.contains(".fifo"));
18950 }
18951
18952 #[test]
18953 fn sns_topic_create_and_delete() {
18954 let prov = make_provisioner();
18955 let res = make_resource(
18956 "AWS::SNS::Topic",
18957 "MyTopic",
18958 serde_json::json!({"TopicName": "t1"}),
18959 );
18960 let sr = prov.create_resource(&res).unwrap();
18961 assert!(sr.physical_id.contains("t1"));
18962 prov.delete_resource(&sr).unwrap();
18963 }
18964
18965 #[test]
18966 fn ssm_parameter_create_and_delete() {
18967 let prov = make_provisioner();
18968 let res = make_resource(
18969 "AWS::SSM::Parameter",
18970 "MyParam",
18971 serde_json::json!({
18972 "Name": "/my/param",
18973 "Type": "String",
18974 "Value": "v1"
18975 }),
18976 );
18977 let sr = prov.create_resource(&res).unwrap();
18978 assert_eq!(sr.physical_id, "/my/param");
18979 prov.delete_resource(&sr).unwrap();
18980 }
18981
18982 #[test]
18983 fn iam_role_create_and_delete() {
18984 let prov = make_provisioner();
18985 let res = make_resource(
18986 "AWS::IAM::Role",
18987 "MyRole",
18988 serde_json::json!({
18989 "RoleName": "my-role",
18990 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
18991 }),
18992 );
18993 let sr = prov.create_resource(&res).unwrap();
18994 assert!(sr.physical_id.contains("my-role"));
18995 prov.delete_resource(&sr).unwrap();
18996 }
18997
18998 #[test]
18999 fn iam_policy_create_and_delete() {
19000 let prov = make_provisioner();
19001 let res = make_resource(
19002 "AWS::IAM::Policy",
19003 "MyPolicy",
19004 serde_json::json!({
19005 "PolicyName": "my-policy",
19006 "PolicyDocument": {"Version": "2012-10-17", "Statement": []}
19007 }),
19008 );
19009 let sr = prov.create_resource(&res).unwrap();
19010 assert!(sr.physical_id.contains("my-policy"));
19011 prov.delete_resource(&sr).unwrap();
19012 }
19013
19014 #[test]
19015 fn s3_bucket_create_and_delete() {
19016 let prov = make_provisioner();
19017 let res = make_resource(
19018 "AWS::S3::Bucket",
19019 "MyBucket",
19020 serde_json::json!({"BucketName": "my-bucket"}),
19021 );
19022 let sr = prov.create_resource(&res).unwrap();
19023 assert_eq!(sr.physical_id, "my-bucket");
19024 prov.delete_resource(&sr).unwrap();
19025 }
19026
19027 #[test]
19028 fn dynamodb_table_create_and_delete() {
19029 let prov = make_provisioner();
19030 let res = make_resource(
19031 "AWS::DynamoDB::Table",
19032 "MyTable",
19033 serde_json::json!({
19034 "TableName": "my-table",
19035 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
19036 "AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
19037 "BillingMode": "PAY_PER_REQUEST"
19038 }),
19039 );
19040 let sr = prov.create_resource(&res).unwrap();
19041 assert!(sr.physical_id.contains("my-table"));
19042 prov.delete_resource(&sr).unwrap();
19043 }
19044
19045 #[test]
19046 fn log_group_create_and_delete() {
19047 let prov = make_provisioner();
19048 let res = make_resource(
19049 "AWS::Logs::LogGroup",
19050 "MyLogs",
19051 serde_json::json!({"LogGroupName": "/app/logs"}),
19052 );
19053 let sr = prov.create_resource(&res).unwrap();
19054 assert!(sr.physical_id.contains("/app/logs"));
19055 prov.delete_resource(&sr).unwrap();
19056 }
19057
19058 #[test]
19059 fn lambda_function_create_and_delete() {
19060 let prov = make_provisioner();
19061 let res = make_resource(
19062 "AWS::Lambda::Function",
19063 "MyFn",
19064 serde_json::json!({
19065 "FunctionName": "my-fn",
19066 "Runtime": "nodejs20.x",
19067 "Role": "arn:aws:iam::123456789012:role/lambda-role",
19068 "Handler": "index.handler",
19069 "MemorySize": 256,
19070 "Timeout": 10,
19071 "Environment": {"Variables": {"FOO": "bar"}}
19072 }),
19073 );
19074 let sr = prov.create_resource(&res).unwrap();
19075 assert_eq!(sr.physical_id, "my-fn");
19076 assert_eq!(
19077 sr.attributes.get("Arn").map(String::as_str),
19078 Some("arn:aws:lambda:us-east-1:123456789012:function:my-fn")
19079 );
19080 {
19082 let lam = prov.lambda_state.read();
19083 let st = lam.get("123456789012").unwrap();
19084 let f = st.functions.get("my-fn").unwrap();
19085 assert_eq!(f.runtime, "nodejs20.x");
19086 assert_eq!(f.memory_size, 256);
19087 assert_eq!(f.environment.get("FOO").unwrap(), "bar");
19088 }
19089 prov.delete_resource(&sr).unwrap();
19090 let lam = prov.lambda_state.read();
19091 let st = lam.get("123456789012").unwrap();
19092 assert!(!st.functions.contains_key("my-fn"));
19093 }
19094
19095 #[test]
19096 fn unsupported_resource_type_fails() {
19097 let prov = make_provisioner();
19098 let res = make_resource("AWS::NonExistent::Thing", "X", serde_json::json!({}));
19099 assert!(prov.create_resource(&res).is_err());
19100 }
19101
19102 #[test]
19103 fn iam_role_with_inline_policies() {
19104 let prov = make_provisioner();
19105 let res = make_resource(
19106 "AWS::IAM::Role",
19107 "MyRole",
19108 serde_json::json!({
19109 "RoleName": "role-inline",
19110 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []},
19111 "Policies": [
19112 {
19113 "PolicyName": "inline-1",
19114 "PolicyDocument": {"Version": "2012-10-17", "Statement": []}
19115 }
19116 ]
19117 }),
19118 );
19119 let sr = prov.create_resource(&res).unwrap();
19120 assert!(sr.physical_id.contains("role-inline"));
19121 }
19122
19123 #[test]
19124 fn sqs_queue_auto_name() {
19125 let prov = make_provisioner();
19126 let res = make_resource("AWS::SQS::Queue", "AutoQ", serde_json::json!({}));
19127 let sr = prov.create_resource(&res).unwrap();
19128 assert!(!sr.physical_id.is_empty());
19130 }
19131
19132 #[test]
19133 fn sns_topic_auto_name() {
19134 let prov = make_provisioner();
19135 let res = make_resource("AWS::SNS::Topic", "AutoT", serde_json::json!({}));
19136 let sr = prov.create_resource(&res).unwrap();
19137 assert!(!sr.physical_id.is_empty());
19138 }
19139
19140 #[test]
19143 fn unsupported_resource_type_errors() {
19144 let prov = make_provisioner();
19145 let res = make_resource("AWS::FooBar::Thing", "X", serde_json::json!({}));
19146 assert!(prov.create_resource(&res).is_err());
19147 }
19148
19149 #[test]
19150 fn sqs_queue_with_redrive_policy() {
19151 let prov = make_provisioner();
19152 let dlq = make_resource(
19154 "AWS::SQS::Queue",
19155 "DLQ",
19156 serde_json::json!({"QueueName": "dlq1"}),
19157 );
19158 let dlq_resource = prov.create_resource(&dlq).unwrap();
19159 let _ = dlq_resource.physical_id;
19160
19161 let src = make_resource(
19163 "AWS::SQS::Queue",
19164 "Src",
19165 serde_json::json!({
19166 "QueueName": "src1",
19167 "RedrivePolicy": {
19168 "deadLetterTargetArn": "arn:aws:sqs:us-east-1:123456789012:dlq1",
19169 "maxReceiveCount": 3
19170 }
19171 }),
19172 );
19173 let sr = prov.create_resource(&src).unwrap();
19174 assert!(!sr.physical_id.is_empty());
19175 }
19176
19177 #[test]
19178 fn sns_topic_with_display_name() {
19179 let prov = make_provisioner();
19180 let res = make_resource(
19181 "AWS::SNS::Topic",
19182 "WithName",
19183 serde_json::json!({"TopicName": "named-topic", "DisplayName": "Named"}),
19184 );
19185 let sr = prov.create_resource(&res).unwrap();
19186 assert!(sr.physical_id.contains("named-topic"));
19187 }
19188
19189 #[test]
19190 fn ssm_parameter_with_explicit_name() {
19191 let prov = make_provisioner();
19192 let res = make_resource(
19193 "AWS::SSM::Parameter",
19194 "Param",
19195 serde_json::json!({"Name": "/my/param", "Value": "v", "Type": "String"}),
19196 );
19197 let sr = prov.create_resource(&res).unwrap();
19198 assert!(sr.physical_id.contains("/my/param"));
19199 }
19200
19201 #[test]
19202 fn ssm_parameter_missing_name_errors() {
19203 let prov = make_provisioner();
19204 let res = make_resource(
19205 "AWS::SSM::Parameter",
19206 "AutoP",
19207 serde_json::json!({"Value": "v", "Type": "String"}),
19208 );
19209 assert!(prov.create_resource(&res).is_err());
19210 }
19211
19212 #[test]
19213 fn iam_managed_policy_auto_name() {
19214 let prov = make_provisioner();
19215 let res = make_resource(
19216 "AWS::IAM::Policy",
19217 "AutoPol",
19218 serde_json::json!({
19219 "PolicyName": "inline-pol",
19220 "PolicyDocument": {"Version": "2012-10-17", "Statement": []},
19221 "Users": []
19222 }),
19223 );
19224 let sr = prov.create_resource(&res).unwrap();
19225 assert!(!sr.physical_id.is_empty());
19226 }
19227
19228 #[test]
19229 fn delete_resource_works_for_queue() {
19230 let prov = make_provisioner();
19231 let res = make_resource(
19232 "AWS::SQS::Queue",
19233 "ToDel",
19234 serde_json::json!({"QueueName": "todel"}),
19235 );
19236 let sr = prov.create_resource(&res).unwrap();
19237 assert!(prov.delete_resource(&sr).is_ok());
19238 }
19239
19240 #[test]
19241 fn delete_resource_works_for_topic() {
19242 let prov = make_provisioner();
19243 let res = make_resource(
19244 "AWS::SNS::Topic",
19245 "DelT",
19246 serde_json::json!({"TopicName": "delt"}),
19247 );
19248 let sr = prov.create_resource(&res).unwrap();
19249 assert!(prov.delete_resource(&sr).is_ok());
19250 }
19251
19252 #[test]
19253 fn application_autoscaling_scalable_target_round_trip() {
19254 let prov = make_provisioner();
19255 let res = make_resource(
19256 "AWS::ApplicationAutoScaling::ScalableTarget",
19257 "Target",
19258 serde_json::json!({
19259 "ServiceNamespace": "ecs",
19260 "ResourceId": "service/my-cluster/my-service",
19261 "ScalableDimension": "ecs:service:DesiredCount",
19262 "MinCapacity": 1,
19263 "MaxCapacity": 10,
19264 "RoleARN": "arn:aws:iam::123456789012:role/my-role",
19265 }),
19266 );
19267 let sr = prov.create_resource(&res).unwrap();
19268 assert_eq!(sr.physical_id, "service/my-cluster/my-service");
19269 assert!(sr.attributes.contains_key("ScalableTargetARN"));
19270 assert!(prov.delete_resource(&sr).is_ok());
19271 }
19272
19273 #[test]
19274 fn application_autoscaling_scaling_policy_requires_target() {
19275 let prov = make_provisioner();
19276 let res = make_resource(
19277 "AWS::ApplicationAutoScaling::ScalingPolicy",
19278 "Policy",
19279 serde_json::json!({
19280 "PolicyName": "my-policy",
19281 "ServiceNamespace": "ecs",
19282 "ResourceId": "service/my-cluster/my-service",
19283 "ScalableDimension": "ecs:service:DesiredCount",
19284 "PolicyType": "TargetTrackingScaling",
19285 "TargetTrackingScalingPolicyConfiguration": {
19286 "TargetValue": 50.0,
19287 "PredefinedMetricSpecification": {
19288 "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
19289 }
19290 },
19291 }),
19292 );
19293 assert!(prov.create_resource(&res).is_err());
19295 }
19296
19297 #[test]
19298 fn application_autoscaling_scaling_policy_round_trip() {
19299 let prov = make_provisioner();
19300 let target = make_resource(
19301 "AWS::ApplicationAutoScaling::ScalableTarget",
19302 "Target",
19303 serde_json::json!({
19304 "ServiceNamespace": "ecs",
19305 "ResourceId": "service/my-cluster/my-service",
19306 "ScalableDimension": "ecs:service:DesiredCount",
19307 "MinCapacity": 1,
19308 "MaxCapacity": 10,
19309 }),
19310 );
19311 let sr = prov.create_resource(&target).unwrap();
19312
19313 let policy = make_resource(
19314 "AWS::ApplicationAutoScaling::ScalingPolicy",
19315 "Policy",
19316 serde_json::json!({
19317 "PolicyName": "my-policy",
19318 "ServiceNamespace": "ecs",
19319 "ResourceId": "service/my-cluster/my-service",
19320 "ScalableDimension": "ecs:service:DesiredCount",
19321 "PolicyType": "TargetTrackingScaling",
19322 "TargetTrackingScalingPolicyConfiguration": {
19323 "TargetValue": 50.0,
19324 "PredefinedMetricSpecification": {
19325 "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
19326 }
19327 },
19328 }),
19329 );
19330 let psr = prov.create_resource(&policy).unwrap();
19331 assert!(psr.physical_id.starts_with("arn:aws:autoscaling:"));
19332 assert!(prov.delete_resource(&psr).is_ok());
19333 assert!(prov.delete_resource(&sr).is_ok());
19334 }
19335
19336 #[test]
19337 fn sqs_queue_with_fifo_suffix() {
19338 let prov = make_provisioner();
19339 let res = make_resource(
19340 "AWS::SQS::Queue",
19341 "Fifo",
19342 serde_json::json!({"QueueName": "fq.fifo", "FifoQueue": true}),
19343 );
19344 let sr = prov.create_resource(&res).unwrap();
19345 assert!(sr.physical_id.ends_with(".fifo"));
19346 }
19347
19348 #[test]
19351 fn getatt_s3_bucket_arn_returns_arn() {
19352 let prov = make_provisioner();
19353 let bucket = make_resource(
19354 "AWS::S3::Bucket",
19355 "MyBucket",
19356 serde_json::json!({"BucketName": "my-bucket"}),
19357 );
19358 let sr = prov.create_resource(&bucket).unwrap();
19359 assert_eq!(
19360 prov.get_att(&sr, "Arn"),
19361 Some("arn:aws:s3:::my-bucket".to_string())
19362 );
19363 }
19364
19365 #[test]
19366 fn getatt_s3_bucket_domain_name_returns_dns_name() {
19367 let prov = make_provisioner();
19368 let bucket = make_resource(
19369 "AWS::S3::Bucket",
19370 "MyBucket",
19371 serde_json::json!({"BucketName": "my-bucket"}),
19372 );
19373 let sr = prov.create_resource(&bucket).unwrap();
19374 assert_eq!(
19375 prov.get_att(&sr, "DomainName"),
19376 Some("my-bucket.s3.amazonaws.com".to_string())
19377 );
19378 }
19379
19380 #[test]
19381 fn getatt_lambda_function_arn_returns_function_arn() {
19382 let prov = make_provisioner();
19383 let role = make_resource(
19385 "AWS::IAM::Role",
19386 "MyRole",
19387 serde_json::json!({
19388 "RoleName": "my-role",
19389 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
19390 }),
19391 );
19392 let role_sr = prov.create_resource(&role).unwrap();
19393 let fn_res = make_resource(
19394 "AWS::Lambda::Function",
19395 "MyFn",
19396 serde_json::json!({
19397 "FunctionName": "my-fn",
19398 "Runtime": "python3.11",
19399 "Handler": "index.handler",
19400 "Role": role_sr.physical_id,
19401 "Code": {"ZipFile": "def handler(e,c): return e"}
19402 }),
19403 );
19404 let fn_sr = prov.create_resource(&fn_res).unwrap();
19405 let arn = prov.get_att(&fn_sr, "Arn").expect("Arn should resolve");
19406 assert!(arn.starts_with("arn:aws:lambda:"));
19407 assert!(arn.contains(":function:my-fn"));
19408 }
19409
19410 #[test]
19411 fn getatt_iam_role_arn_returns_role_arn() {
19412 let prov = make_provisioner();
19413 let role = make_resource(
19414 "AWS::IAM::Role",
19415 "MyRole",
19416 serde_json::json!({
19417 "RoleName": "my-role",
19418 "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
19419 }),
19420 );
19421 let sr = prov.create_resource(&role).unwrap();
19422 assert_eq!(
19423 prov.get_att(&sr, "Arn"),
19424 Some("arn:aws:iam::123456789012:role/my-role".to_string())
19425 );
19426 let role_id = prov.get_att(&sr, "RoleId").expect("RoleId should resolve");
19428 assert!(role_id.starts_with("FKIA"));
19429 }
19430
19431 #[test]
19432 fn getatt_unknown_attribute_returns_none() {
19433 let prov = make_provisioner();
19434 let bucket = make_resource(
19435 "AWS::S3::Bucket",
19436 "MyBucket",
19437 serde_json::json!({"BucketName": "my-bucket"}),
19438 );
19439 let sr = prov.create_resource(&bucket).unwrap();
19440 assert_eq!(prov.get_att(&sr, "NotARealAttr"), None);
19444 }
19445
19446 #[test]
19447 fn getatt_unknown_resource_type_returns_none() {
19448 let prov = make_provisioner();
19449 let stack_resource = StackResource {
19453 logical_id: "Mystery".to_string(),
19454 physical_id: "mystery-id".to_string(),
19455 resource_type: "AWS::Made::Up".to_string(),
19456 status: "CREATE_COMPLETE".to_string(),
19457 service_token: None,
19458 attributes: BTreeMap::new(),
19459 };
19460 assert_eq!(prov.get_att(&stack_resource, "Arn"), None);
19461 }
19462
19463 #[test]
19464 fn getatt_falls_back_to_captured_attributes() {
19465 let prov = make_provisioner();
19466 let stack_resource = StackResource {
19470 logical_id: "MyTopic".to_string(),
19471 physical_id: "arn:aws:sns:us-east-1:123456789012:my-topic".to_string(),
19472 resource_type: "AWS::SNS::Topic".to_string(),
19473 status: "CREATE_COMPLETE".to_string(),
19474 service_token: None,
19475 attributes: {
19476 let mut m = BTreeMap::new();
19477 m.insert("TopicArn".to_string(), "captured-arn".to_string());
19478 m
19479 },
19480 };
19481 assert_eq!(
19482 prov.get_att(&stack_resource, "TopicArn"),
19483 Some("captured-arn".to_string())
19484 );
19485 }
19486
19487 #[test]
19488 fn getatt_secrets_manager_arn_resolves_via_live_state() {
19489 let prov = make_provisioner();
19492 let res = make_resource(
19493 "AWS::SecretsManager::Secret",
19494 "MySecret",
19495 serde_json::json!({"Name": "my-secret", "SecretString": "hunter2"}),
19496 );
19497 let sr = prov.create_resource(&res).unwrap();
19498 let arn = prov.get_att(&sr, "Arn").expect("Arn should resolve");
19499 assert!(arn.starts_with("arn:aws:secretsmanager:"));
19500 assert!(arn.ends_with(":secret:my-secret"));
19501 }
19502
19503 #[test]
19504 fn wafv2_web_acl_lifecycle() {
19505 let prov = make_provisioner();
19506 let res = make_resource(
19507 "AWS::WAFv2::WebACL",
19508 "MyAcl",
19509 serde_json::json!({
19510 "Name": "my-acl",
19511 "Scope": "REGIONAL",
19512 "DefaultAction": {"Allow": {}},
19513 "Rules": [{"Name": "rule1", "Priority": 1, "Statement": {}, "VisibilityConfig": {}}],
19514 "VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "my-acl-metric"},
19515 "Capacity": 100,
19516 }),
19517 );
19518 let sr = prov.create_resource(&res).unwrap();
19519 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
19520 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
19521 assert_eq!(prov.get_att(&sr, "Name"), Some("my-acl".to_string()));
19522 assert!(prov.get_att(&sr, "Id").is_some());
19523 assert_eq!(prov.get_att(&sr, "Capacity"), Some("100".to_string()));
19524
19525 prov.delete_resource(&sr.clone()).unwrap();
19526 let fresh = StackResource {
19529 logical_id: "MyAcl".to_string(),
19530 physical_id: sr.physical_id.clone(),
19531 resource_type: "AWS::WAFv2::WebACL".to_string(),
19532 status: "CREATE_COMPLETE".to_string(),
19533 service_token: None,
19534 attributes: BTreeMap::new(),
19535 };
19536 assert_eq!(prov.get_att(&fresh, "Arn"), None);
19537 }
19538
19539 #[test]
19540 fn wafv2_ip_set_lifecycle() {
19541 let prov = make_provisioner();
19542 let res = make_resource(
19543 "AWS::WAFv2::IPSet",
19544 "MyIpSet",
19545 serde_json::json!({
19546 "Name": "my-ipset",
19547 "Scope": "REGIONAL",
19548 "IPAddressVersion": "IPV4",
19549 "Addresses": ["10.0.0.0/8"],
19550 }),
19551 );
19552 let sr = prov.create_resource(&res).unwrap();
19553 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
19554 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
19555 assert_eq!(prov.get_att(&sr, "Name"), Some("my-ipset".to_string()));
19556
19557 prov.delete_resource(&sr.clone()).unwrap();
19558 let fresh = StackResource {
19559 logical_id: "MyIpSet".to_string(),
19560 physical_id: sr.physical_id.clone(),
19561 resource_type: "AWS::WAFv2::IPSet".to_string(),
19562 status: "CREATE_COMPLETE".to_string(),
19563 service_token: None,
19564 attributes: BTreeMap::new(),
19565 };
19566 assert_eq!(prov.get_att(&fresh, "Arn"), None);
19567 }
19568
19569 #[test]
19570 fn wafv2_regex_pattern_set_lifecycle() {
19571 let prov = make_provisioner();
19572 let res = make_resource(
19573 "AWS::WAFv2::RegexPatternSet",
19574 "MyRegexSet",
19575 serde_json::json!({
19576 "Name": "my-regex",
19577 "Scope": "REGIONAL",
19578 "RegularExpressions": [{"RegexString": "^test"}],
19579 }),
19580 );
19581 let sr = prov.create_resource(&res).unwrap();
19582 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
19583 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
19584 assert_eq!(prov.get_att(&sr, "Name"), Some("my-regex".to_string()));
19585
19586 prov.delete_resource(&sr.clone()).unwrap();
19587 let fresh = StackResource {
19588 logical_id: "MyRegexSet".to_string(),
19589 physical_id: sr.physical_id.clone(),
19590 resource_type: "AWS::WAFv2::RegexPatternSet".to_string(),
19591 status: "CREATE_COMPLETE".to_string(),
19592 service_token: None,
19593 attributes: BTreeMap::new(),
19594 };
19595 assert_eq!(prov.get_att(&fresh, "Arn"), None);
19596 }
19597
19598 #[test]
19599 fn wafv2_rule_group_lifecycle() {
19600 let prov = make_provisioner();
19601 let res = make_resource(
19602 "AWS::WAFv2::RuleGroup",
19603 "MyRuleGroup",
19604 serde_json::json!({
19605 "Name": "my-rg",
19606 "Scope": "REGIONAL",
19607 "Capacity": 50,
19608 "Rules": [{"Name": "r1", "Priority": 1, "Statement": {}, "VisibilityConfig": {}}],
19609 "VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "rg-metric"},
19610 }),
19611 );
19612 let sr = prov.create_resource(&res).unwrap();
19613 assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
19614 assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
19615 assert_eq!(prov.get_att(&sr, "Name"), Some("my-rg".to_string()));
19616
19617 prov.delete_resource(&sr.clone()).unwrap();
19618 let fresh = StackResource {
19619 logical_id: "MyRuleGroup".to_string(),
19620 physical_id: sr.physical_id.clone(),
19621 resource_type: "AWS::WAFv2::RuleGroup".to_string(),
19622 status: "CREATE_COMPLETE".to_string(),
19623 service_token: None,
19624 attributes: BTreeMap::new(),
19625 };
19626 assert_eq!(prov.get_att(&fresh, "Arn"), None);
19627 }
19628
19629 #[test]
19630 fn wafv2_logging_configuration_lifecycle() {
19631 let prov = make_provisioner();
19632 let res = make_resource(
19633 "AWS::WAFv2::LoggingConfiguration",
19634 "MyLogConfig",
19635 serde_json::json!({
19636 "ResourceArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/abc",
19637 "LogDestinationConfigs": ["arn:aws:logs:us-east-1:123456789012:log-group:/aws/waf"],
19638 }),
19639 );
19640 let sr = prov.create_resource(&res).unwrap();
19641 assert_eq!(
19642 sr.physical_id,
19643 "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/abc"
19644 );
19645
19646 prov.delete_resource(&sr.clone()).unwrap();
19647 }
19648
19649 #[test]
19650 fn wafv2_web_acl_association_lifecycle() {
19651 let prov = make_provisioner();
19652 let res = make_resource(
19653 "AWS::WAFv2::WebACLAssociation",
19654 "MyAssoc",
19655 serde_json::json!({
19656 "ResourceArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188",
19657 "WebACLArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/my-acl/abc",
19658 }),
19659 );
19660 let sr = prov.create_resource(&res).unwrap();
19661 assert_eq!(sr.physical_id, "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188");
19662
19663 prov.delete_resource(&sr.clone()).unwrap();
19664 }
19665
19666 #[test]
19667 fn ses_configuration_set_lifecycle() {
19668 let prov = make_provisioner();
19669 let res = make_resource(
19670 "AWS::SES::ConfigurationSet",
19671 "MyConfigSet",
19672 serde_json::json!({
19673 "Name": "my-cs",
19674 "SendingOptions": {"SendingEnabled": true},
19675 "DeliveryOptions": {"TlsPolicy": "REQUIRE"},
19676 }),
19677 );
19678 let sr = prov.create_resource(&res).unwrap();
19679 assert_eq!(sr.physical_id, "my-cs");
19680 assert_eq!(prov.get_att(&sr, "Name"), Some("my-cs".to_string()));
19681
19682 prov.delete_resource(&sr.clone()).unwrap();
19683 let fresh = StackResource {
19684 logical_id: "MyConfigSet".to_string(),
19685 physical_id: "my-cs".to_string(),
19686 resource_type: "AWS::SES::ConfigurationSet".to_string(),
19687 status: "CREATE_COMPLETE".to_string(),
19688 service_token: None,
19689 attributes: BTreeMap::new(),
19690 };
19691 assert_eq!(prov.get_att(&fresh, "Name"), None);
19692 }
19693
19694 #[test]
19695 fn ses_email_identity_lifecycle() {
19696 let prov = make_provisioner();
19697 let res = make_resource(
19698 "AWS::SES::EmailIdentity",
19699 "MyIdentity",
19700 serde_json::json!({"EmailIdentity": "example.com"}),
19701 );
19702 let sr = prov.create_resource(&res).unwrap();
19703 assert_eq!(sr.physical_id, "example.com");
19704 assert_eq!(
19705 prov.get_att(&sr, "IdentityName"),
19706 Some("example.com".to_string())
19707 );
19708
19709 prov.delete_resource(&sr.clone()).unwrap();
19710 let fresh = StackResource {
19711 logical_id: "MyIdentity".to_string(),
19712 physical_id: "example.com".to_string(),
19713 resource_type: "AWS::SES::EmailIdentity".to_string(),
19714 status: "CREATE_COMPLETE".to_string(),
19715 service_token: None,
19716 attributes: BTreeMap::new(),
19717 };
19718 assert_eq!(prov.get_att(&fresh, "IdentityName"), None);
19719 }
19720
19721 #[test]
19722 fn ses_template_lifecycle() {
19723 let prov = make_provisioner();
19724 let res = make_resource(
19725 "AWS::SES::Template",
19726 "MyTemplate",
19727 serde_json::json!({
19728 "Template": {
19729 "TemplateName": "my-tpl",
19730 "SubjectPart": "Hello",
19731 "HtmlPart": "<h1>Hi</h1>",
19732 "TextPart": "Hi",
19733 },
19734 }),
19735 );
19736 let sr = prov.create_resource(&res).unwrap();
19737 assert_eq!(sr.physical_id, "my-tpl");
19738 assert_eq!(
19739 prov.get_att(&sr, "TemplateName"),
19740 Some("my-tpl".to_string())
19741 );
19742
19743 prov.delete_resource(&sr.clone()).unwrap();
19744 let fresh = StackResource {
19745 logical_id: "MyTemplate".to_string(),
19746 physical_id: "my-tpl".to_string(),
19747 resource_type: "AWS::SES::Template".to_string(),
19748 status: "CREATE_COMPLETE".to_string(),
19749 service_token: None,
19750 attributes: BTreeMap::new(),
19751 };
19752 assert_eq!(prov.get_att(&fresh, "TemplateName"), None);
19753 }
19754
19755 #[test]
19756 fn ses_contact_list_lifecycle() {
19757 let prov = make_provisioner();
19758 let res = make_resource(
19759 "AWS::SES::ContactList",
19760 "MyContactList",
19761 serde_json::json!({
19762 "ContactListName": "my-cl",
19763 "Description": "Test contacts",
19764 "Topics": [{"TopicName": "news", "DisplayName": "Newsletter", "Description": "Weekly news"}],
19765 }),
19766 );
19767 let sr = prov.create_resource(&res).unwrap();
19768 assert_eq!(sr.physical_id, "my-cl");
19769 assert_eq!(
19770 prov.get_att(&sr, "ContactListName"),
19771 Some("my-cl".to_string())
19772 );
19773
19774 prov.delete_resource(&sr.clone()).unwrap();
19775 let fresh = StackResource {
19776 logical_id: "MyContactList".to_string(),
19777 physical_id: "my-cl".to_string(),
19778 resource_type: "AWS::SES::ContactList".to_string(),
19779 status: "CREATE_COMPLETE".to_string(),
19780 service_token: None,
19781 attributes: BTreeMap::new(),
19782 };
19783 assert_eq!(prov.get_att(&fresh, "ContactListName"), None);
19784 }
19785
19786 #[test]
19787 fn ses_dedicated_ip_pool_lifecycle() {
19788 let prov = make_provisioner();
19789 let res = make_resource(
19790 "AWS::SES::DedicatedIpPool",
19791 "MyPool",
19792 serde_json::json!({"PoolName": "my-pool", "ScalingMode": "STANDARD"}),
19793 );
19794 let sr = prov.create_resource(&res).unwrap();
19795 assert_eq!(sr.physical_id, "my-pool");
19796 assert_eq!(prov.get_att(&sr, "PoolName"), Some("my-pool".to_string()));
19797
19798 prov.delete_resource(&sr.clone()).unwrap();
19799 let fresh = StackResource {
19800 logical_id: "MyPool".to_string(),
19801 physical_id: "my-pool".to_string(),
19802 resource_type: "AWS::SES::DedicatedIpPool".to_string(),
19803 status: "CREATE_COMPLETE".to_string(),
19804 service_token: None,
19805 attributes: BTreeMap::new(),
19806 };
19807 assert_eq!(prov.get_att(&fresh, "PoolName"), None);
19808 }
19809
19810 #[test]
19811 fn ses_receipt_rule_set_lifecycle() {
19812 let prov = make_provisioner();
19813 let res = make_resource(
19814 "AWS::SES::ReceiptRuleSet",
19815 "MyRuleSet",
19816 serde_json::json!({"RuleSetName": "my-rs"}),
19817 );
19818 let sr = prov.create_resource(&res).unwrap();
19819 assert_eq!(sr.physical_id, "my-rs");
19820 assert_eq!(prov.get_att(&sr, "RuleSetName"), Some("my-rs".to_string()));
19821
19822 prov.delete_resource(&sr.clone()).unwrap();
19823 let fresh = StackResource {
19824 logical_id: "MyRuleSet".to_string(),
19825 physical_id: "my-rs".to_string(),
19826 resource_type: "AWS::SES::ReceiptRuleSet".to_string(),
19827 status: "CREATE_COMPLETE".to_string(),
19828 service_token: None,
19829 attributes: BTreeMap::new(),
19830 };
19831 assert_eq!(prov.get_att(&fresh, "RuleSetName"), None);
19832 }
19833
19834 #[test]
19835 fn ses_receipt_rule_lifecycle() {
19836 let prov = make_provisioner();
19837 let rs = make_resource(
19838 "AWS::SES::ReceiptRuleSet",
19839 "MyRuleSet",
19840 serde_json::json!({"RuleSetName": "my-rs2"}),
19841 );
19842 prov.create_resource(&rs).unwrap();
19843
19844 let res = make_resource(
19845 "AWS::SES::ReceiptRule",
19846 "MyRule",
19847 serde_json::json!({
19848 "RuleSetName": "my-rs2",
19849 "Rule": {
19850 "Name": "rule1",
19851 "Priority": 1,
19852 "Enabled": true,
19853 "Actions": [{"S3Action": {"BucketName": "my-bucket"}}],
19854 },
19855 }),
19856 );
19857 let sr = prov.create_resource(&res).unwrap();
19858 assert_eq!(sr.physical_id, "my-rs2|rule1");
19859
19860 prov.delete_resource(&sr.clone()).unwrap();
19861 }
19862
19863 #[test]
19864 fn ses_receipt_filter_lifecycle() {
19865 let prov = make_provisioner();
19866 let res = make_resource(
19867 "AWS::SES::ReceiptFilter",
19868 "MyFilter",
19869 serde_json::json!({
19870 "Filter": {
19871 "Name": "my-filter",
19872 "IpFilter": {"Policy": "Block", "Cidr": "10.0.0.0/8"},
19873 },
19874 }),
19875 );
19876 let sr = prov.create_resource(&res).unwrap();
19877 assert_eq!(sr.physical_id, "my-filter");
19878
19879 prov.delete_resource(&sr.clone()).unwrap();
19880 }
19881
19882 #[test]
19883 fn ses_vdm_attributes_lifecycle() {
19884 let prov = make_provisioner();
19885 let res = make_resource(
19886 "AWS::SES::VdmAttributes",
19887 "MyVdm",
19888 serde_json::json!({
19889 "DashboardAttributes": {"EngagementMetrics": "ENABLED"},
19890 "GuardianAttributes": {"OptimizedSharedDelivery": "ENABLED"},
19891 }),
19892 );
19893 let sr = prov.create_resource(&res).unwrap();
19894 assert_eq!(sr.physical_id, "vdm-MyVdm");
19895
19896 prov.delete_resource(&sr.clone()).unwrap();
19897 }
19898
19899 #[test]
19900 fn athena_work_group_lifecycle() {
19901 let prov = make_provisioner();
19902 let res = make_resource(
19903 "AWS::Athena::WorkGroup",
19904 "MyWg",
19905 serde_json::json!({
19906 "Name": "my-wg",
19907 "Description": "test wg",
19908 "Configuration": {"EnforceWorkGroupConfiguration": true},
19909 }),
19910 );
19911 let sr = prov.create_resource(&res).unwrap();
19912 assert_eq!(sr.physical_id, "my-wg");
19913 assert_eq!(sr.attributes.get("Name"), Some(&"my-wg".to_string()));
19914 assert!(sr
19915 .attributes
19916 .get("Arn")
19917 .unwrap()
19918 .contains("workgroup/my-wg"));
19919
19920 assert_eq!(
19921 prov.get_att(
19922 &StackResource {
19923 resource_type: "AWS::Athena::WorkGroup".to_string(),
19924 physical_id: sr.physical_id.clone(),
19925 logical_id: "MyWg".to_string(),
19926 status: "CREATE_COMPLETE".to_string(),
19927 service_token: None,
19928 attributes: BTreeMap::new(),
19929 },
19930 "Name",
19931 ),
19932 Some("my-wg".to_string()),
19933 );
19934
19935 prov.delete_resource(&sr.clone()).unwrap();
19936 }
19937
19938 #[test]
19939 fn athena_data_catalog_lifecycle() {
19940 let prov = make_provisioner();
19941 let res = make_resource(
19942 "AWS::Athena::DataCatalog",
19943 "MyCatalog",
19944 serde_json::json!({
19945 "Name": "my-catalog",
19946 "Type": "GLUE",
19947 "Description": "test catalog",
19948 }),
19949 );
19950 let sr = prov.create_resource(&res).unwrap();
19951 assert_eq!(sr.physical_id, "my-catalog");
19952 assert_eq!(sr.attributes.get("Name"), Some(&"my-catalog".to_string()));
19953 assert!(sr
19954 .attributes
19955 .get("Arn")
19956 .unwrap()
19957 .contains("datacatalog/my-catalog"));
19958
19959 prov.delete_resource(&sr.clone()).unwrap();
19960 }
19961
19962 #[test]
19963 fn athena_named_query_lifecycle() {
19964 let prov = make_provisioner();
19965 let res = make_resource(
19966 "AWS::Athena::NamedQuery",
19967 "MyQuery",
19968 serde_json::json!({
19969 "Name": "my-query",
19970 "Database": "mydb",
19971 "QueryString": "SELECT 1",
19972 "WorkGroup": "primary",
19973 }),
19974 );
19975 let sr = prov.create_resource(&res).unwrap();
19976 assert!(!sr.physical_id.is_empty());
19977 assert_eq!(sr.attributes.get("NamedQueryId"), Some(&sr.physical_id));
19978
19979 prov.delete_resource(&sr.clone()).unwrap();
19980 }
19981
19982 #[test]
19983 fn athena_prepared_statement_lifecycle() {
19984 let prov = make_provisioner();
19985 let res = make_resource(
19986 "AWS::Athena::PreparedStatement",
19987 "MyPs",
19988 serde_json::json!({
19989 "StatementName": "my-ps",
19990 "WorkGroupName": "primary",
19991 "QueryStatement": "SELECT 1",
19992 }),
19993 );
19994 let sr = prov.create_resource(&res).unwrap();
19995 assert_eq!(sr.physical_id, "primary|my-ps");
19996
19997 prov.delete_resource(&sr.clone()).unwrap();
19998 }
19999}