use chrono::Utc;
use parking_lot::RwLock;
use std::collections::BTreeMap;
use std::sync::Arc;
use uuid::Uuid;
use crate::state::SharedCloudFormationState;
use fakecloud_acm::{
CertificateOptions as AcmCertificateOptions, DomainValidation as AcmDomainValidation,
RenewalSummary as AcmRenewalSummary, SharedAcmState, StoredCertificate as AcmStoredCertificate,
};
use fakecloud_apigateway::{
make_id as apigw_make_id, ApiKey as ApiGwApiKey, Authorizer as ApiGwAuthorizer,
Deployment as ApiGwDeployment, Integration as ApiGwIntegration, Method as ApiGwMethod,
Model as ApiGwModel, Resource as ApiGwResource, RestApi as ApiGwRestApi, SharedApiGatewayState,
Stage as ApiGwStage, UsagePlan as ApiGwUsagePlan,
};
use fakecloud_apigatewayv2::{
Authorizer as ApiGwV2Authorizer, CorsConfiguration as ApiGwV2CorsConfiguration,
Deployment as ApiGwV2Deployment, HttpApi as ApiGwV2HttpApi, Integration as ApiGwV2Integration,
JwtConfiguration as ApiGwV2JwtConfiguration, Route as ApiGwV2Route, SharedApiGatewayV2State,
Stage as ApiGwV2Stage,
};
use fakecloud_application_autoscaling::{
ScalableTarget as AppasScalableTarget, ScalingPolicy as AppasScalingPolicy,
SharedApplicationAutoScalingState as AppasState, SuspendedState as AppasSuspendedState,
};
use fakecloud_athena::{DataCatalog, NamedQuery, PreparedStatement, SharedAthenaState, WorkGroup};
use fakecloud_cloudfront::{
functions::{
CloudFrontOriginAccessIdentityConfig, FunctionConfig, KeyGroupConfig, KeyGroupItems,
PublicKeyConfig, StoredFunction, StoredKeyGroup, StoredOriginAccessIdentity,
StoredPublicKey,
},
model::{
DefaultCacheBehavior, DistributionConfig, Origin, OriginItems, Origins, ViewerCertificate,
},
policies::{
CachePolicyConfig, OriginAccessControlConfig, OriginRequestPolicyConfig,
OriginRequestPolicyCookiesConfig, OriginRequestPolicyHeadersConfig,
OriginRequestPolicyQueryStringsConfig, ResponseHeadersPolicyConfig, StoredCachePolicy,
StoredOriginAccessControl, StoredOriginRequestPolicy, StoredResponseHeadersPolicy,
},
state::StoredDistribution,
SharedCloudFrontState,
};
use fakecloud_cloudwatch::{AlarmState, Dashboard, MetricAlarm, SharedCloudWatchState};
use fakecloud_cognito::{
default_schema_attributes, AccountRecoverySetting, AdminCreateUserConfig,
CognitoIdentityProvider, CustomDomainConfig, EmailConfiguration, IdentityPool,
IdentityPoolRoleAttachment, PasswordPolicy, PoolPolicies, RecoveryOption, SchemaAttribute,
SharedCognitoState, SignInPolicy, SmsConfiguration, UserPool, UserPoolClient, UserPoolDomain,
};
use fakecloud_core::delivery::DeliveryBus;
use fakecloud_dynamodb::{
AttributeDefinition, DynamoTable, KeySchemaElement, OnDemandThroughput, ProvisionedThroughput,
SharedDynamoDbState,
};
use fakecloud_ecr::{Repository, SharedEcrState};
use fakecloud_ecs::{
CapacityProvider as EcsCapacityProvider, Cluster as EcsCluster, Service as EcsService,
SharedEcsState, TagEntry as EcsTagEntry, TaskDefinition as EcsTaskDefinition,
};
use fakecloud_elasticache::{
CacheCluster as EcCacheCluster, CacheParameterGroup, CacheSecurityGroup, CacheSubnetGroup,
ElastiCacheUser as EcUser, ElastiCacheUserGroup as EcUserGroup,
ReplicationGroup as EcReplicationGroup, SharedElastiCacheState,
};
use fakecloud_elbv2::{
Action as ElbAction, Listener, LoadBalancer, Rule as ElbRule, RuleCondition, SharedElbv2State,
Tag as ElbTag, TargetGroup, TargetGroupTuple,
};
use fakecloud_eventbridge::{
ApiDestination, Archive, Connection, Endpoint, EventBus, EventRule, SharedEventBridgeState,
};
use fakecloud_firehose::state::{DeliveryStream, S3Destination};
use fakecloud_iam::{
IamAccessKey, IamGroup, IamInstanceProfile, IamPolicy, IamRole, IamUser, OidcProvider,
PolicyVersion, SamlProvider, SharedIamState, Tag, VirtualMfaDevice,
};
use fakecloud_kinesis::{build_stream_shards, KinesisConsumer, KinesisStream, SharedKinesisState};
use fakecloud_kms::provisioner as kms_provisioner;
use fakecloud_kms::SharedKmsState;
use fakecloud_lambda::{
AttachedLayer, EventSourceMapping, FunctionAlias, FunctionUrlConfig, Layer, LayerVersion,
SharedLambdaState,
};
use fakecloud_logs::{
Delivery, DeliveryDestination, DeliverySource, Destination, LogStream, MetricFilter,
MetricTransformation, QueryDefinition, ResourcePolicy, SharedLogsState, SubscriptionFilter,
};
use fakecloud_organizations::{
OrganizationState, OrganizationalUnit, Policy as OrgPolicy, SharedOrganizationsState,
POLICY_TYPE_SCP,
};
use fakecloud_rds::{DbInstance, DbParameterGroup, DbSubnetGroup, RdsTag, SharedRdsState};
use fakecloud_route53::{
model::{HealthCheckConfig, HostedZoneFeatures, ResourceRecordSet},
SharedRoute53State, StoredHealthCheck, StoredHostedZone,
};
use fakecloud_s3::{S3Bucket, SharedS3State};
use fakecloud_secretsmanager::{RotationRules, Secret, SecretVersion, SharedSecretsManagerState};
use fakecloud_ses::{
ConfigurationSet as SesConfigurationSet, ContactList as SesContactList,
DedicatedIpPool as SesDedicatedIpPool, EmailIdentity as SesEmailIdentity,
EmailTemplate as SesEmailTemplate, EventDestination as SesEventDestination,
IpFilter as SesIpFilter, ReceiptAction as SesReceiptAction, ReceiptFilter as SesReceiptFilter,
ReceiptRule as SesReceiptRule, ReceiptRuleSet as SesReceiptRuleSet, SharedSesState,
};
use fakecloud_sns::{SharedSnsState, SnsSubscription, SnsTopic};
use fakecloud_sqs::{SharedSqsState, SqsQueue};
use fakecloud_ssm::{SharedSsmState, SsmParameter};
use fakecloud_stepfunctions::{
Activity as SfnActivity, AliasRoute, SharedStepFunctionsState, StateMachine, StateMachineAlias,
StateMachineStatus, StateMachineType, StateMachineVersion,
};
use fakecloud_wafv2::{IpSet, RegexPatternSet, RuleGroup, SharedWafv2State, WebAcl};
use crate::state::StackResource;
use crate::template::ResourceDefinition;
fn parse_iam_tags(value: Option<&serde_json::Value>) -> Vec<Tag> {
let Some(arr) = value.and_then(|v| v.as_array()) else {
return Vec::new();
};
arr.iter()
.filter_map(|t| {
let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
Some(Tag { key, value })
})
.collect()
}
fn parse_elb_tags(value: Option<&serde_json::Value>) -> Vec<ElbTag> {
let Some(arr) = value.and_then(|v| v.as_array()) else {
return Vec::new();
};
arr.iter()
.filter_map(|t| {
let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
Some(ElbTag { key, value })
})
.collect()
}
fn parse_elb_actions(value: Option<&serde_json::Value>) -> Vec<ElbAction> {
let Some(arr) = value.and_then(|v| v.as_array()) else {
return Vec::new();
};
arr.iter()
.map(|a| {
let action_type = a
.get("Type")
.and_then(|v| v.as_str())
.unwrap_or("forward")
.to_string();
let target_group_arn = a
.get("TargetGroupArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let order = a.get("Order").and_then(|v| v.as_i64()).map(|n| n as i32);
let redirect = a
.get("RedirectConfig")
.map(|r| fakecloud_elbv2::RedirectConfig {
protocol: r
.get("Protocol")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
port: r
.get("Port")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
host: r
.get("Host")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
path: r
.get("Path")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
query: r
.get("Query")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
status_code: r
.get("StatusCode")
.and_then(|v| v.as_str())
.unwrap_or("HTTP_302")
.to_string(),
});
let fixed_response =
a.get("FixedResponseConfig")
.map(|f| fakecloud_elbv2::FixedResponseConfig {
message_body: f
.get("MessageBody")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
status_code: f
.get("StatusCode")
.and_then(|v| v.as_str())
.unwrap_or("200")
.to_string(),
content_type: f
.get("ContentType")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
});
let forward = a.get("ForwardConfig").map(|f| {
let target_groups: Vec<TargetGroupTuple> = f
.get("TargetGroups")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|t| {
let target_group_arn = t
.get("TargetGroupArn")
.and_then(|v| v.as_str())?
.to_string();
let weight =
t.get("Weight").and_then(|v| v.as_i64()).map(|n| n as i32);
Some(TargetGroupTuple {
target_group_arn,
weight,
})
})
.collect()
})
.unwrap_or_default();
fakecloud_elbv2::ForwardConfig {
target_groups,
stickiness: None,
}
});
ElbAction {
action_type,
target_group_arn,
order,
redirect,
fixed_response,
forward,
authenticate_cognito: None,
authenticate_oidc: None,
}
})
.collect()
}
fn parse_elb_rule_conditions(value: Option<&serde_json::Value>) -> Vec<RuleCondition> {
let Some(arr) = value.and_then(|v| v.as_array()) else {
return Vec::new();
};
arr.iter()
.map(|c| {
let field = c
.get("Field")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let values: Vec<String> = c
.get("Values")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|s| s.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let host_header_values: Vec<String> = c
.get("HostHeaderConfig")
.and_then(|v| v.get("Values"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|s| s.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
RuleCondition {
field,
values,
host_header_values,
path_pattern_values: Vec::new(),
http_header_name: None,
http_header_values: Vec::new(),
query_string_values: Vec::new(),
http_request_method_values: Vec::new(),
source_ip_values: Vec::new(),
}
})
.collect()
}
fn parse_key_policy(props: &serde_json::Value) -> Option<String> {
match props.get("KeyPolicy") {
Some(v) if v.is_string() => Some(v.as_str().unwrap_or("").to_string()),
Some(v) => Some(serde_json::to_string(v).unwrap_or_default()),
None => None,
}
}
fn parse_tag_list(props: &serde_json::Value) -> BTreeMap<String, String> {
let mut tags: BTreeMap<String, String> = BTreeMap::new();
if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
for t in arr {
if let (Some(k), Some(v)) = (
t.get("Key").and_then(|x| x.as_str()),
t.get("Value").and_then(|x| x.as_str()),
) {
tags.insert(k.to_string(), v.to_string());
}
}
}
tags
}
fn parse_kms_key_input(props: &serde_json::Value) -> kms_provisioner::KeyCreationInput {
kms_provisioner::KeyCreationInput {
description: props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
key_usage: props
.get("KeyUsage")
.and_then(|v| v.as_str())
.unwrap_or("ENCRYPT_DECRYPT")
.to_string(),
key_spec: props
.get("KeySpec")
.and_then(|v| v.as_str())
.unwrap_or("SYMMETRIC_DEFAULT")
.to_string(),
origin: props
.get("Origin")
.and_then(|v| v.as_str())
.unwrap_or("AWS_KMS")
.to_string(),
enabled: props
.get("Enabled")
.and_then(|v| v.as_bool())
.unwrap_or(true),
multi_region: props
.get("MultiRegion")
.and_then(|v| v.as_bool())
.unwrap_or(false),
key_rotation_enabled: props
.get("EnableKeyRotation")
.and_then(|v| v.as_bool())
.unwrap_or(false),
policy: parse_key_policy(props),
tags: parse_tag_list(props),
}
}
fn parse_log_group_name(input: &str) -> String {
if let Some(rest) = input.strip_prefix("arn:aws:logs:") {
if let Some(after) = rest.split(":log-group:").nth(1) {
return after.trim_end_matches(":*").to_string();
}
}
input.to_string()
}
fn parse_lambda_function_name(input: &str) -> String {
if let Some(rest) = input.strip_prefix("arn:aws:lambda:") {
if let Some(after) = rest.split(":function:").nth(1) {
return after.split(':').next().unwrap_or(after).to_string();
}
}
input.to_string()
}
struct LambdaFunctionProps {
runtime: String,
role: String,
handler: String,
description: String,
timeout: i64,
memory_size: i64,
package_type: String,
tags: BTreeMap<String, String>,
environment: BTreeMap<String, String>,
architectures: Vec<String>,
code_zip: Option<Vec<u8>>,
s3_bucket: Option<String>,
s3_key: Option<String>,
image_uri: Option<String>,
layers: Vec<String>,
tracing_mode: Option<String>,
kms_key_arn: Option<String>,
ephemeral_storage_size: Option<i64>,
vpc_config: Option<serde_json::Value>,
snap_start: Option<serde_json::Value>,
dead_letter_config_arn: Option<String>,
file_system_configs: Vec<serde_json::Value>,
logging_config: Option<serde_json::Value>,
}
fn parse_lambda_function_props(props: &serde_json::Value) -> Result<LambdaFunctionProps, String> {
let runtime = props
.get("Runtime")
.and_then(|v| v.as_str())
.unwrap_or("python3.12")
.to_string();
let role = props
.get("Role")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let handler = props
.get("Handler")
.and_then(|v| v.as_str())
.unwrap_or("index.handler")
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let timeout = props.get("Timeout").and_then(|v| v.as_i64()).unwrap_or(3);
let memory_size = props
.get("MemorySize")
.and_then(|v| v.as_i64())
.unwrap_or(128);
let architectures = props
.get("Architectures")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect::<Vec<_>>()
})
.unwrap_or_else(|| vec!["x86_64".to_string()]);
let package_type = props
.get("PackageType")
.and_then(|v| v.as_str())
.unwrap_or("Zip")
.to_string();
let environment = props
.get("Environment")
.and_then(|v| v.get("Variables"))
.and_then(|v| v.as_object())
.map(|o| {
o.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect::<BTreeMap<String, String>>()
})
.unwrap_or_default();
let tags: BTreeMap<String, String> = props
.get("Tags")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|t| {
let k = t.get("Key").and_then(|v| v.as_str())?.to_string();
let v = t.get("Value").and_then(|v| v.as_str())?.to_string();
Some((k, v))
})
.collect()
})
.unwrap_or_default();
let code = props.get("Code");
let code_zip = code
.and_then(|c| c.get("ZipFile"))
.and_then(|v| v.as_str())
.map(|s| s.as_bytes().to_vec());
let s3_bucket = code
.and_then(|c| c.get("S3Bucket"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let s3_key = code
.and_then(|c| c.get("S3Key"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let image_uri = if package_type == "Image" {
code.and_then(|c| c.get("ImageUri"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
} else {
None
};
if package_type == "Image" && image_uri.is_none() {
return Err("Code.ImageUri is required when PackageType is Image".to_string());
}
let layers: Vec<String> = props
.get("Layers")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let tracing_mode = props
.get("TracingConfig")
.and_then(|v| v.get("Mode"))
.and_then(|v| v.as_str())
.map(String::from);
let kms_key_arn = props
.get("KmsKeyArn")
.and_then(|v| v.as_str())
.map(String::from);
let ephemeral_storage_size = props
.get("EphemeralStorage")
.and_then(|v| v.get("Size"))
.and_then(|v| v.as_i64());
let vpc_config = props.get("VpcConfig").filter(|v| v.is_object()).cloned();
let snap_start = props.get("SnapStart").filter(|v| v.is_object()).cloned();
let dead_letter_config_arn = props
.get("DeadLetterConfig")
.and_then(|v| v.get("TargetArn"))
.and_then(|v| v.as_str())
.map(String::from);
let file_system_configs = props
.get("FileSystemConfigs")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let logging_config = props
.get("LoggingConfig")
.filter(|v| v.is_object())
.cloned();
Ok(LambdaFunctionProps {
runtime,
role,
handler,
description,
timeout,
memory_size,
package_type,
tags,
environment,
architectures,
code_zip,
s3_bucket,
s3_key,
image_uri,
layers,
tracing_mode,
kms_key_arn,
ephemeral_storage_size,
vpc_config,
snap_start,
dead_letter_config_arn,
file_system_configs,
logging_config,
})
}
struct LambdaEventSourceMappingProps {
event_source_arn: String,
batch_size: i64,
enabled: bool,
starting_position: Option<String>,
starting_position_timestamp: Option<f64>,
parallelization_factor: Option<i64>,
maximum_batching_window_in_seconds: Option<i64>,
function_response_types: Vec<String>,
filter_patterns: Vec<String>,
kms_key_arn: Option<String>,
metrics_config: Option<serde_json::Value>,
destination_config: Option<serde_json::Value>,
maximum_retry_attempts: Option<i64>,
maximum_record_age_in_seconds: Option<i64>,
bisect_batch_on_function_error: Option<bool>,
tumbling_window_in_seconds: Option<i64>,
topics: Vec<String>,
queues: Vec<String>,
}
fn parse_lambda_event_source_mapping_props(
props: &serde_json::Value,
) -> Result<LambdaEventSourceMappingProps, String> {
let event_source_arn = props
.get("EventSourceArn")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let batch_size = props
.get("BatchSize")
.and_then(|v| v.as_i64())
.unwrap_or(10);
let enabled = props
.get("Enabled")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let starting_position = props
.get("StartingPosition")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let starting_position_timestamp = props
.get("StartingPositionTimestamp")
.and_then(|v| v.as_f64());
let parallelization_factor = props.get("ParallelizationFactor").and_then(|v| v.as_i64());
let maximum_batching_window_in_seconds = props
.get("MaximumBatchingWindowInSeconds")
.and_then(|v| v.as_i64());
let function_response_types: Vec<String> = props
.get("FunctionResponseTypes")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let filter_patterns: Vec<String> = props
.get("FilterCriteria")
.and_then(|v| v.get("Filters"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|f| {
f.get("Pattern")
.and_then(|p| p.as_str())
.map(|s| s.to_string())
})
.collect()
})
.unwrap_or_default();
let kms_key_arn = props
.get("KmsKeyArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let metrics_config = props
.get("MetricsConfig")
.filter(|v| v.is_object())
.cloned();
let destination_config = props
.get("DestinationConfig")
.filter(|v| v.is_object())
.cloned();
let maximum_retry_attempts = props.get("MaximumRetryAttempts").and_then(|v| v.as_i64());
let maximum_record_age_in_seconds = props
.get("MaximumRecordAgeInSeconds")
.and_then(|v| v.as_i64());
let bisect_batch_on_function_error = props
.get("BisectBatchOnFunctionError")
.and_then(|v| v.as_bool());
let tumbling_window_in_seconds = props
.get("TumblingWindowInSeconds")
.and_then(|v| v.as_i64());
let topics: Vec<String> = props
.get("Topics")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let queues: Vec<String> = props
.get("Queues")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
Ok(LambdaEventSourceMappingProps {
event_source_arn,
batch_size,
enabled,
starting_position,
starting_position_timestamp,
parallelization_factor,
maximum_batching_window_in_seconds,
function_response_types,
filter_patterns,
kms_key_arn,
metrics_config,
destination_config,
maximum_retry_attempts,
maximum_record_age_in_seconds,
bisect_batch_on_function_error,
tumbling_window_in_seconds,
topics,
queues,
})
}
fn sha256_b64(bytes: &[u8]) -> String {
use sha2::Digest;
let hash = sha2::Sha256::digest(bytes);
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash)
}
fn layer_code_size(
accounts: &fakecloud_core::multi_account::MultiAccountState<fakecloud_lambda::LambdaState>,
arn: &str,
) -> i64 {
let Some(rest) = arn.strip_prefix("arn:aws:lambda:") else {
return 0;
};
let mut parts = rest.split(':');
let _region = parts.next();
let Some(account) = parts.next() else {
return 0;
};
if parts.next() != Some("layer") {
return 0;
}
let Some(name) = parts.next() else {
return 0;
};
let Some(ver_str) = parts.next() else {
return 0;
};
let Ok(ver) = ver_str.parse::<i64>() else {
return 0;
};
accounts
.get(account)
.and_then(|s| s.layers.get(name))
.and_then(|l| l.versions.iter().find(|v| v.version == ver))
.map(|v| v.code_size)
.unwrap_or(0)
}
pub struct ProvisionResult {
pub physical_id: String,
pub attributes: BTreeMap<String, String>,
}
impl ProvisionResult {
pub fn new(physical_id: impl Into<String>) -> Self {
Self {
physical_id: physical_id.into(),
attributes: BTreeMap::new(),
}
}
pub fn with(mut self, key: &str, value: impl Into<String>) -> Self {
self.attributes.insert(key.to_string(), value.into());
self
}
pub fn merge_attributes(mut self, other: BTreeMap<String, String>) -> Self {
for (k, v) in other {
self.attributes.insert(k, v);
}
self
}
}
pub struct ResourceProvisioner {
pub sqs_state: SharedSqsState,
pub sns_state: SharedSnsState,
pub ssm_state: SharedSsmState,
pub iam_state: SharedIamState,
pub s3_state: SharedS3State,
pub eventbridge_state: SharedEventBridgeState,
pub dynamodb_state: SharedDynamoDbState,
pub logs_state: SharedLogsState,
pub lambda_state: SharedLambdaState,
pub secretsmanager_state: SharedSecretsManagerState,
pub kinesis_state: SharedKinesisState,
pub kms_state: SharedKmsState,
pub ecr_state: SharedEcrState,
pub cloudwatch_state: SharedCloudWatchState,
pub elbv2_state: SharedElbv2State,
pub organizations_state: SharedOrganizationsState,
pub cognito_state: SharedCognitoState,
pub rds_state: SharedRdsState,
pub ecs_state: SharedEcsState,
pub acm_state: SharedAcmState,
pub elasticache_state: SharedElastiCacheState,
pub route53_state: SharedRoute53State,
pub cloudfront_state: SharedCloudFrontState,
pub stepfunctions_state: SharedStepFunctionsState,
pub wafv2_state: SharedWafv2State,
pub apigateway_state: SharedApiGatewayState,
pub apigatewayv2_state: SharedApiGatewayV2State,
pub ses_state: SharedSesState,
pub app_autoscaling_state: AppasState,
pub athena_state: SharedAthenaState,
pub firehose_state: fakecloud_firehose::SharedFirehoseState,
pub glue_state: fakecloud_glue::SharedGlueState,
pub cloudformation_state: SharedCloudFormationState,
pub delivery: Arc<DeliveryBus>,
pub account_id: String,
pub region: String,
pub stack_id: String,
}
impl ResourceProvisioner {
pub fn create_resource(&self, resource: &ResourceDefinition) -> Result<StackResource, String> {
let result = match resource.resource_type.as_str() {
"AWS::SQS::Queue" => self.create_sqs_queue(resource),
"AWS::SNS::Topic" => self.create_sns_topic(resource),
"AWS::SNS::Subscription" => self.create_sns_subscription(resource),
"AWS::SSM::Parameter" => self.create_ssm_parameter(resource),
"AWS::IAM::Role" => self.create_iam_role(resource),
"AWS::IAM::Policy" => self.create_iam_policy(resource),
"AWS::IAM::User" => self.create_iam_user(resource),
"AWS::IAM::Group" => self.create_iam_group(resource),
"AWS::IAM::ManagedPolicy" => self.create_iam_managed_policy(resource),
"AWS::IAM::UserToGroupAddition" => self.create_iam_user_to_group_addition(resource),
"AWS::IAM::AccessKey" => self.create_iam_access_key(resource),
"AWS::IAM::InstanceProfile" => self.create_iam_instance_profile(resource),
"AWS::IAM::OIDCProvider" => self.create_iam_oidc_provider(resource),
"AWS::IAM::SAMLProvider" => self.create_iam_saml_provider(resource),
"AWS::IAM::ServiceLinkedRole" => self.create_iam_service_linked_role(resource),
"AWS::IAM::VirtualMFADevice" => self.create_iam_virtual_mfa_device(resource),
"AWS::S3::Bucket" => self.create_s3_bucket(resource),
"AWS::Events::Rule" => self.create_eventbridge_rule(resource),
"AWS::Events::Connection" => self.create_eventbridge_connection(resource),
"AWS::Events::ApiDestination" => self.create_eventbridge_api_destination(resource),
"AWS::Events::Archive" => self.create_eventbridge_archive(resource),
"AWS::Events::EventBus" => self.create_eventbridge_event_bus(resource),
"AWS::Events::EventBusPolicy" => self.create_eventbridge_event_bus_policy(resource),
"AWS::Events::Endpoint" => self.create_eventbridge_endpoint(resource),
"AWS::DynamoDB::Table" => self.create_dynamodb_table(resource),
"AWS::Logs::LogGroup" => self.create_log_group(resource),
"AWS::Logs::LogStream" => self.create_log_stream(resource),
"AWS::Logs::MetricFilter" => self.create_metric_filter(resource),
"AWS::Logs::SubscriptionFilter" => self.create_subscription_filter(resource),
"AWS::Logs::Destination" => self.create_logs_destination(resource),
"AWS::Logs::ResourcePolicy" => self.create_logs_resource_policy(resource),
"AWS::Logs::QueryDefinition" => self.create_logs_query_definition(resource),
"AWS::Logs::Delivery" => self.create_logs_delivery(resource),
"AWS::Logs::DeliveryDestination" => self.create_logs_delivery_destination(resource),
"AWS::Logs::DeliverySource" => self.create_logs_delivery_source(resource),
"AWS::Lambda::Function" => self.create_lambda_function(resource),
"AWS::Lambda::Permission" => self.create_lambda_permission(resource),
"AWS::Lambda::EventSourceMapping" => self.create_lambda_event_source_mapping(resource),
"AWS::Lambda::LayerVersion" => self.create_lambda_layer_version(resource),
"AWS::Lambda::Url" => self.create_lambda_url(resource),
"AWS::Lambda::Alias" => self.create_lambda_alias(resource),
"AWS::Lambda::Version" => self.create_lambda_version(resource),
"AWS::SecretsManager::Secret" => self.create_secrets_manager_secret(resource),
"AWS::Kinesis::Stream" => self.create_kinesis_stream(resource),
"AWS::Kinesis::StreamConsumer" => self.create_kinesis_stream_consumer(resource),
"AWS::KMS::Key" => self.create_kms_key(resource),
"AWS::KMS::Alias" => self.create_kms_alias(resource),
"AWS::KMS::ReplicaKey" => self.create_kms_replica_key(resource),
"AWS::ECR::Repository" => self.create_ecr_repository(resource),
"AWS::ECR::RepositoryPolicy" => self.create_ecr_repository_policy(resource),
"AWS::ECR::LifecyclePolicy" => self.create_ecr_lifecycle_policy(resource),
"AWS::ECR::RegistryPolicy" => self.create_ecr_registry_policy(resource),
"AWS::ECR::ReplicationConfiguration" => {
self.create_ecr_replication_configuration(resource)
}
"AWS::ECR::RegistryScanningConfiguration" => {
self.create_ecr_registry_scanning_configuration(resource)
}
"AWS::ECR::PullThroughCacheRule" => self.create_ecr_pull_through_cache_rule(resource),
"AWS::CloudWatch::Alarm" => self.create_cloudwatch_alarm(resource),
"AWS::CloudWatch::Dashboard" => self.create_cloudwatch_dashboard(resource),
"AWS::ElasticLoadBalancingV2::LoadBalancer" => {
self.create_elbv2_load_balancer(resource)
}
"AWS::ElasticLoadBalancingV2::TargetGroup" => self.create_elbv2_target_group(resource),
"AWS::ElasticLoadBalancingV2::Listener" => self.create_elbv2_listener(resource),
"AWS::ElasticLoadBalancingV2::ListenerRule" => {
self.create_elbv2_listener_rule(resource)
}
"AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
self.create_elbv2_listener_certificate(resource)
}
"AWS::ElasticLoadBalancingV2::TrustStore" => self.create_elbv2_trust_store(resource),
"AWS::Organizations::Organization" => self.create_organization(resource),
"AWS::Organizations::OrganizationalUnit" => self.create_organization_unit(resource),
"AWS::Organizations::Account" => self.create_organization_account(resource),
"AWS::Organizations::Policy" => self.create_organization_policy(resource),
"AWS::Organizations::ResourcePolicy" => {
self.create_organization_resource_policy(resource)
}
"AWS::Cognito::UserPool" => self.create_cognito_user_pool(resource),
"AWS::Cognito::UserPoolClient" => self.create_cognito_user_pool_client(resource),
"AWS::Cognito::UserPoolDomain" => self.create_cognito_user_pool_domain(resource),
"AWS::Cognito::IdentityPool" => self.create_cognito_identity_pool(resource),
"AWS::Cognito::IdentityPoolRoleAttachment" => {
self.create_cognito_identity_pool_role_attachment(resource)
}
"AWS::RDS::DBSubnetGroup" => self.create_rds_subnet_group(resource),
"AWS::RDS::DBParameterGroup" => self.create_rds_parameter_group(resource),
"AWS::RDS::DBClusterParameterGroup" => {
self.create_rds_cluster_parameter_group(resource)
}
"AWS::RDS::OptionGroup" => self.create_rds_option_group(resource),
"AWS::RDS::EventSubscription" => self.create_rds_event_subscription(resource),
"AWS::RDS::DBSecurityGroup" => self.create_rds_security_group(resource),
"AWS::RDS::DBProxy" => self.create_rds_db_proxy(resource),
"AWS::RDS::DBInstance" => self.create_rds_db_instance(resource),
"AWS::RDS::DBCluster" => self.create_rds_db_cluster(resource),
"AWS::ECS::Cluster" => self.create_ecs_cluster(resource),
"AWS::ECS::TaskDefinition" => self.create_ecs_task_definition(resource),
"AWS::ECS::Service" => self.create_ecs_service(resource),
"AWS::ECS::CapacityProvider" => self.create_ecs_capacity_provider(resource),
"AWS::CertificateManager::Certificate" => self.create_acm_certificate(resource),
"AWS::CertificateManager::Account" => self.create_acm_account(resource),
"AWS::ElastiCache::ParameterGroup" => self.create_ec_parameter_group(resource),
"AWS::ElastiCache::SubnetGroup" => self.create_ec_subnet_group(resource),
"AWS::ElastiCache::SecurityGroup" => self.create_ec_security_group(resource),
"AWS::ElastiCache::User" => self.create_ec_user(resource),
"AWS::ElastiCache::UserGroup" => self.create_ec_user_group(resource),
"AWS::ElastiCache::CacheCluster" => self.create_ec_cache_cluster(resource),
"AWS::ElastiCache::ReplicationGroup" => self.create_ec_replication_group(resource),
"AWS::Route53::HostedZone" => self.create_route53_hosted_zone(resource),
"AWS::Route53::RecordSet" => self.create_route53_record_set(resource),
"AWS::Route53::HealthCheck" => self.create_route53_health_check(resource),
"AWS::Route53::DNSSEC" => self.create_route53_dnssec(resource),
"AWS::Route53::KeySigningKey" => self.create_route53_key_signing_key(resource),
"AWS::CloudFront::CloudFrontOriginAccessIdentity" => {
self.create_cf_origin_access_identity(resource)
}
"AWS::CloudFront::Distribution" => self.create_cf_distribution(resource),
"AWS::CloudFront::OriginAccessControl" => {
self.create_cf_origin_access_control(resource)
}
"AWS::CloudFront::PublicKey" => self.create_cf_public_key(resource),
"AWS::CloudFront::KeyGroup" => self.create_cf_key_group(resource),
"AWS::CloudFront::Function" => self.create_cf_function(resource),
"AWS::CloudFront::CachePolicy" => self.create_cf_cache_policy(resource),
"AWS::CloudFront::OriginRequestPolicy" => {
self.create_cf_origin_request_policy(resource)
}
"AWS::CloudFront::ResponseHeadersPolicy" => {
self.create_cf_response_headers_policy(resource)
}
"AWS::StepFunctions::StateMachine" => self.create_sfn_state_machine(resource),
"AWS::StepFunctions::Activity" => self.create_sfn_activity(resource),
"AWS::StepFunctions::StateMachineVersion" => self.create_sfn_version(resource),
"AWS::StepFunctions::StateMachineAlias" => self.create_sfn_alias(resource),
"AWS::WAFv2::WebACL" => self.create_wafv2_web_acl(resource),
"AWS::WAFv2::IPSet" => self.create_wafv2_ip_set(resource),
"AWS::WAFv2::RegexPatternSet" => self.create_wafv2_regex_pattern_set(resource),
"AWS::WAFv2::RuleGroup" => self.create_wafv2_rule_group(resource),
"AWS::WAFv2::LoggingConfiguration" => self.create_wafv2_logging_configuration(resource),
"AWS::WAFv2::WebACLAssociation" => self.create_wafv2_web_acl_association(resource),
"AWS::ApiGateway::RestApi" => self.create_apigw_rest_api(resource),
"AWS::ApiGateway::Resource" => self.create_apigw_resource(resource),
"AWS::ApiGateway::Method" => self.create_apigw_method(resource),
"AWS::ApiGateway::Deployment" => self.create_apigw_deployment(resource),
"AWS::ApiGateway::Stage" => self.create_apigw_stage(resource),
"AWS::ApiGateway::Authorizer" => self.create_apigw_authorizer(resource),
"AWS::ApiGateway::RequestValidator" => self.create_apigw_request_validator(resource),
"AWS::ApiGateway::Model" => self.create_apigw_model(resource),
"AWS::ApiGateway::GatewayResponse" => self.create_apigw_gateway_response(resource),
"AWS::ApiGateway::UsagePlan" => self.create_apigw_usage_plan(resource),
"AWS::ApiGateway::ApiKey" => self.create_apigw_api_key(resource),
"AWS::ApiGateway::UsagePlanKey" => self.create_apigw_usage_plan_key(resource),
"AWS::ApiGateway::DomainName" => self.create_apigw_domain_name(resource),
"AWS::ApiGateway::BasePathMapping" => self.create_apigw_base_path_mapping(resource),
"AWS::ApiGatewayV2::Api" => self.create_apigwv2_api(resource),
"AWS::ApiGatewayV2::Route" => self.create_apigwv2_route(resource),
"AWS::ApiGatewayV2::Integration" => self.create_apigwv2_integration(resource),
"AWS::ApiGatewayV2::IntegrationResponse" => {
self.create_apigwv2_integration_response(resource)
}
"AWS::ApiGatewayV2::RouteResponse" => self.create_apigwv2_route_response(resource),
"AWS::ApiGatewayV2::Stage" => self.create_apigwv2_stage(resource),
"AWS::ApiGatewayV2::Deployment" => self.create_apigwv2_deployment(resource),
"AWS::ApiGatewayV2::Authorizer" => self.create_apigwv2_authorizer(resource),
"AWS::ApiGatewayV2::DomainName" => self.create_apigwv2_domain_name(resource),
"AWS::ApiGatewayV2::ApiMapping" => self.create_apigwv2_api_mapping(resource),
"AWS::ApiGatewayV2::VpcLink" => self.create_apigwv2_vpc_link(resource),
"AWS::ApiGatewayV2::Model" => self.create_apigwv2_model(resource),
"AWS::SES::ConfigurationSet" => self.create_ses_configuration_set(resource),
"AWS::SES::ConfigurationSetEventDestination" => {
self.create_ses_event_destination(resource)
}
"AWS::SES::EmailIdentity" => self.create_ses_email_identity(resource),
"AWS::SES::Template" => self.create_ses_template(resource),
"AWS::SES::ContactList" => self.create_ses_contact_list(resource),
"AWS::SES::DedicatedIpPool" => self.create_ses_dedicated_ip_pool(resource),
"AWS::SES::ReceiptRule" => self.create_ses_receipt_rule(resource),
"AWS::SES::ReceiptRuleSet" => self.create_ses_receipt_rule_set(resource),
"AWS::SES::ReceiptFilter" => self.create_ses_receipt_filter(resource),
"AWS::SES::VdmAttributes" => self.create_ses_vdm_attributes(resource),
"AWS::SecretsManager::RotationSchedule" => {
self.create_secrets_manager_rotation_schedule(resource)
}
"AWS::SecretsManager::ResourcePolicy" => {
self.create_secrets_manager_resource_policy(resource)
}
"AWS::SecretsManager::SecretTargetAttachment" => {
self.create_secrets_manager_target_attachment(resource)
}
"AWS::ApplicationAutoScaling::ScalableTarget" => {
self.create_application_autoscaling_scalable_target(resource)
}
"AWS::ApplicationAutoScaling::ScalingPolicy" => {
self.create_application_autoscaling_scaling_policy(resource)
}
"AWS::Athena::DataCatalog" => self.create_athena_data_catalog(resource),
"AWS::Athena::NamedQuery" => self.create_athena_named_query(resource),
"AWS::Athena::WorkGroup" => self.create_athena_work_group(resource),
"AWS::Athena::PreparedStatement" => self.create_athena_prepared_statement(resource),
"AWS::KinesisFirehose::DeliveryStream" => {
self.create_firehose_delivery_stream(resource)
}
"AWS::Glue::Database" => self.create_glue_database(resource),
"AWS::CloudFormation::Stack" => self.create_cloudformation_stack(resource),
"AWS::Glue::Table" => self.create_glue_table(resource),
"AWS::Glue::Partition" => self.create_glue_partition(resource),
t if t.starts_with("Custom::") || t == "AWS::CloudFormation::CustomResource" => self
.create_custom_resource(resource)
.map(ProvisionResult::new),
other => Err(format!("Unsupported resource type: {other}")),
};
let is_custom = resource.resource_type.starts_with("Custom::")
|| resource.resource_type == "AWS::CloudFormation::CustomResource";
let service_token = if is_custom {
resource
.properties
.get("ServiceToken")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
} else {
None
};
result.map(|res| StackResource {
logical_id: resource.logical_id.clone(),
physical_id: res.physical_id,
resource_type: resource.resource_type.clone(),
status: "CREATE_COMPLETE".to_string(),
service_token,
attributes: res.attributes,
})
}
pub fn update_resource(
&self,
existing: &StackResource,
new_def: &ResourceDefinition,
) -> Result<Option<StackResource>, String> {
let result = match new_def.resource_type.as_str() {
"AWS::Lambda::Function" => Some(self.update_lambda_function(existing, new_def)?),
"AWS::Lambda::Permission" => Some(self.update_lambda_permission(existing, new_def)?),
"AWS::Lambda::EventSourceMapping" => {
Some(self.update_lambda_event_source_mapping(existing, new_def)?)
}
"AWS::Lambda::LayerVersion" => {
Some(self.update_lambda_layer_version(existing, new_def)?)
}
"AWS::Lambda::Url" => Some(self.update_lambda_url(existing, new_def)?),
"AWS::Lambda::Alias" => Some(self.update_lambda_alias(existing, new_def)?),
"AWS::Lambda::Version" => Some(self.update_lambda_version(existing, new_def)?),
"AWS::ApiGateway::RestApi" => Some(self.update_apigw_rest_api(existing, new_def)?),
"AWS::ApiGateway::Resource" => Some(self.update_apigw_resource(existing, new_def)?),
"AWS::ApiGateway::Method" => Some(self.update_apigw_method(existing, new_def)?),
"AWS::ApiGateway::Deployment" => Some(self.update_apigw_deployment(existing, new_def)?),
"AWS::ApiGateway::Stage" => Some(self.update_apigw_stage(existing, new_def)?),
"AWS::ApiGateway::Authorizer" => Some(self.update_apigw_authorizer(existing, new_def)?),
"AWS::ApiGateway::RequestValidator" => {
Some(self.update_apigw_request_validator(existing, new_def)?)
}
"AWS::ApiGateway::Model" => Some(self.update_apigw_model(existing, new_def)?),
"AWS::ApiGateway::GatewayResponse" => {
Some(self.update_apigw_gateway_response(existing, new_def)?)
}
"AWS::ApiGateway::UsagePlan" => Some(self.update_apigw_usage_plan(existing, new_def)?),
"AWS::ApiGateway::ApiKey" => Some(self.update_apigw_api_key(existing, new_def)?),
"AWS::ApiGateway::UsagePlanKey" => {
Some(self.update_apigw_usage_plan_key(existing, new_def)?)
}
"AWS::ApiGateway::DomainName" => {
Some(self.update_apigw_domain_name(existing, new_def)?)
}
"AWS::ApiGateway::BasePathMapping" => {
Some(self.update_apigw_base_path_mapping(existing, new_def)?)
}
"AWS::ApiGatewayV2::Api" => Some(self.update_apigwv2_api(existing, new_def)?),
"AWS::ApiGatewayV2::Route" => Some(self.update_apigwv2_route(existing, new_def)?),
"AWS::ApiGatewayV2::Integration" => {
Some(self.update_apigwv2_integration(existing, new_def)?)
}
"AWS::ApiGatewayV2::IntegrationResponse" => {
Some(self.update_apigwv2_integration_response(existing, new_def)?)
}
"AWS::ApiGatewayV2::RouteResponse" => {
Some(self.update_apigwv2_route_response(existing, new_def)?)
}
"AWS::ApiGatewayV2::Stage" => Some(self.update_apigwv2_stage(existing, new_def)?),
"AWS::ApiGatewayV2::Deployment" => {
Some(self.update_apigwv2_deployment(existing, new_def)?)
}
"AWS::ApiGatewayV2::Authorizer" => {
Some(self.update_apigwv2_authorizer(existing, new_def)?)
}
"AWS::ApiGatewayV2::DomainName" => {
Some(self.update_apigwv2_domain_name(existing, new_def)?)
}
"AWS::ApiGatewayV2::ApiMapping" => {
Some(self.update_apigwv2_api_mapping(existing, new_def)?)
}
"AWS::ApiGatewayV2::VpcLink" => Some(self.update_apigwv2_vpc_link(existing, new_def)?),
"AWS::ApiGatewayV2::Model" => Some(self.update_apigwv2_model(existing, new_def)?),
"AWS::ECS::Cluster" => Some(self.update_ecs_cluster(existing, new_def)?),
"AWS::ECS::Service" => Some(self.update_ecs_service(existing, new_def)?),
"AWS::ECS::TaskDefinition" => Some(self.update_ecs_task_definition(existing, new_def)?),
"AWS::ECS::CapacityProvider" => {
Some(self.update_ecs_capacity_provider(existing, new_def)?)
}
"AWS::ECR::Repository" => Some(self.update_ecr_repository(existing, new_def)?),
"AWS::ECR::RepositoryPolicy" => {
Some(self.update_ecr_repository_policy(existing, new_def)?)
}
"AWS::ECR::LifecyclePolicy" => {
Some(self.update_ecr_lifecycle_policy(existing, new_def)?)
}
"AWS::ECR::RegistryPolicy" => Some(self.update_ecr_registry_policy(existing, new_def)?),
"AWS::ECR::ReplicationConfiguration" => {
Some(self.update_ecr_replication_configuration(existing, new_def)?)
}
"AWS::ECR::RegistryScanningConfiguration" => {
Some(self.update_ecr_registry_scanning_configuration(existing, new_def)?)
}
"AWS::ECR::PullThroughCacheRule" => {
Some(self.update_ecr_pull_through_cache_rule(existing, new_def)?)
}
"AWS::KMS::Key" => Some(self.update_kms_key(existing, new_def)?),
"AWS::KMS::ReplicaKey" => Some(self.update_kms_replica_key(existing, new_def)?),
"AWS::KMS::Alias" => Some(self.update_kms_alias(existing, new_def)?),
"AWS::ElasticLoadBalancingV2::LoadBalancer" => {
Some(self.update_elbv2_load_balancer(existing, new_def)?)
}
"AWS::ElasticLoadBalancingV2::TargetGroup" => {
Some(self.update_elbv2_target_group(existing, new_def)?)
}
"AWS::ElasticLoadBalancingV2::Listener" => {
Some(self.update_elbv2_listener(existing, new_def)?)
}
"AWS::ElasticLoadBalancingV2::ListenerRule" => {
Some(self.update_elbv2_listener_rule(existing, new_def)?)
}
"AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
Some(self.update_elbv2_listener_certificate(existing, new_def)?)
}
"AWS::ElasticLoadBalancingV2::TrustStore" => {
Some(self.update_elbv2_trust_store(existing, new_def)?)
}
"AWS::CloudWatch::Alarm" => Some(self.update_cloudwatch_alarm(existing, new_def)?),
"AWS::CloudWatch::Dashboard" => {
Some(self.update_cloudwatch_dashboard(existing, new_def)?)
}
_ => None,
};
Ok(result.map(|res| StackResource {
logical_id: existing.logical_id.clone(),
physical_id: res.physical_id,
resource_type: existing.resource_type.clone(),
status: "UPDATE_COMPLETE".to_string(),
service_token: existing.service_token.clone(),
attributes: res.attributes,
}))
}
pub fn get_att(&self, resource: &StackResource, attribute: &str) -> Option<String> {
if let Some(v) = resource.attributes.get(attribute) {
return Some(v.clone());
}
match resource.resource_type.as_str() {
"AWS::S3::Bucket" => self.get_att_s3_bucket(&resource.physical_id, attribute),
"AWS::Lambda::Function" => {
self.get_att_lambda_function(&resource.physical_id, attribute)
}
"AWS::IAM::Role" => self.get_att_iam_role(&resource.physical_id, attribute),
"AWS::SQS::Queue" => self.get_att_sqs_queue(&resource.physical_id, attribute),
"AWS::SNS::Topic" => self.get_att_sns_topic(&resource.physical_id, attribute),
"AWS::DynamoDB::Table" => self.get_att_dynamodb_table(&resource.physical_id, attribute),
"AWS::KMS::Key" => self.get_att_kms_key(&resource.physical_id, attribute),
"AWS::SecretsManager::Secret" => {
self.get_att_secrets_manager_secret(&resource.physical_id, attribute)
}
"AWS::CloudFront::Distribution" => {
self.get_att_cf_distribution(&resource.physical_id, attribute)
}
"AWS::ECS::Cluster" => self.get_att_ecs_cluster(&resource.physical_id, attribute),
"AWS::ECS::Service" => self.get_att_ecs_service(&resource.physical_id, attribute),
"AWS::ECS::CapacityProvider" => {
self.get_att_ecs_capacity_provider(&resource.physical_id, attribute)
}
"AWS::ECR::Repository" => self.get_att_ecr_repository(&resource.physical_id, attribute),
"AWS::ElasticLoadBalancingV2::LoadBalancer" => {
self.get_att_elbv2_load_balancer(&resource.physical_id, attribute)
}
"AWS::ElasticLoadBalancingV2::TargetGroup" => {
self.get_att_elbv2_target_group(&resource.physical_id, attribute)
}
"AWS::ElasticLoadBalancingV2::Listener" => {
self.get_att_elbv2_listener(&resource.physical_id, attribute)
}
"AWS::ElasticLoadBalancingV2::ListenerRule" => {
self.get_att_elbv2_listener_rule(&resource.physical_id, attribute)
}
"AWS::ElasticLoadBalancingV2::TrustStore" => {
self.get_att_elbv2_trust_store(&resource.physical_id, attribute)
}
"AWS::WAFv2::WebACL" => self.get_att_wafv2_web_acl(&resource.physical_id, attribute),
"AWS::WAFv2::IPSet" => self.get_att_wafv2_ip_set(&resource.physical_id, attribute),
"AWS::WAFv2::RegexPatternSet" => {
self.get_att_wafv2_regex_pattern_set(&resource.physical_id, attribute)
}
"AWS::WAFv2::RuleGroup" => {
self.get_att_wafv2_rule_group(&resource.physical_id, attribute)
}
"AWS::SES::ConfigurationSet" => {
self.get_att_ses_configuration_set(&resource.physical_id, attribute)
}
"AWS::SES::EmailIdentity" => {
self.get_att_ses_email_identity(&resource.physical_id, attribute)
}
"AWS::SES::Template" => self.get_att_ses_template(&resource.physical_id, attribute),
"AWS::SES::ContactList" => {
self.get_att_ses_contact_list(&resource.physical_id, attribute)
}
"AWS::SES::DedicatedIpPool" => {
self.get_att_ses_dedicated_ip_pool(&resource.physical_id, attribute)
}
"AWS::SES::ReceiptRuleSet" => {
self.get_att_ses_receipt_rule_set(&resource.physical_id, attribute)
}
"AWS::Athena::DataCatalog" => {
self.get_att_athena_data_catalog(&resource.physical_id, attribute)
}
"AWS::Athena::NamedQuery" => {
self.get_att_athena_named_query(&resource.physical_id, attribute)
}
"AWS::Athena::WorkGroup" => {
self.get_att_athena_work_group(&resource.physical_id, attribute)
}
"AWS::Athena::PreparedStatement" => {
self.get_att_athena_prepared_statement(&resource.physical_id, attribute)
}
"AWS::CloudFormation::Stack" => {
self.get_att_cloudformation_stack(&resource.physical_id, attribute)
}
_ => None,
}
}
fn get_att_ses_configuration_set(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
let cs = state.configuration_sets.get(physical_id)?;
match attribute {
"Name" => Some(cs.name.clone()),
_ => None,
}
}
fn get_att_ses_email_identity(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
let id = state.identities.get(physical_id)?;
match attribute {
"IdentityName" => Some(id.identity_name.clone()),
_ => None,
}
}
fn get_att_ses_template(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
let tpl = state.templates.get(physical_id)?;
match attribute {
"TemplateName" => Some(tpl.template_name.clone()),
_ => None,
}
}
fn get_att_ses_contact_list(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
let cl = state.contact_lists.get(physical_id)?;
match attribute {
"ContactListName" => Some(cl.contact_list_name.clone()),
_ => None,
}
}
fn get_att_ses_dedicated_ip_pool(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
let pool = state.dedicated_ip_pools.get(physical_id)?;
match attribute {
"PoolName" => Some(pool.pool_name.clone()),
_ => None,
}
}
fn get_att_ses_receipt_rule_set(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
let rs = state.receipt_rule_sets.get(physical_id)?;
match attribute {
"RuleSetName" => Some(rs.name.clone()),
_ => None,
}
}
fn get_att_wafv2_web_acl(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
let acl = state.web_acls.values().find(|a| a.arn == physical_id)?;
match attribute {
"Arn" => Some(acl.arn.clone()),
"Id" => Some(acl.id.clone()),
"Name" => Some(acl.name.clone()),
"LabelNamespace" => Some(acl.label_namespace.clone()),
"Capacity" => Some(acl.capacity.to_string()),
_ => None,
}
}
fn get_att_wafv2_ip_set(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
let ip_set = state.ip_sets.values().find(|i| i.arn == physical_id)?;
match attribute {
"Arn" => Some(ip_set.arn.clone()),
"Id" => Some(ip_set.id.clone()),
"Name" => Some(ip_set.name.clone()),
_ => None,
}
}
fn get_att_wafv2_regex_pattern_set(
&self,
physical_id: &str,
attribute: &str,
) -> Option<String> {
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
let set = state
.regex_pattern_sets
.values()
.find(|r| r.arn == physical_id)?;
match attribute {
"Arn" => Some(set.arn.clone()),
"Id" => Some(set.id.clone()),
"Name" => Some(set.name.clone()),
_ => None,
}
}
fn get_att_wafv2_rule_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
let rg = state.rule_groups.values().find(|r| r.arn == physical_id)?;
match attribute {
"Arn" => Some(rg.arn.clone()),
"Id" => Some(rg.id.clone()),
"Name" => Some(rg.name.clone()),
_ => None,
}
}
fn get_att_s3_bucket(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.s3_state.write();
let state = accounts.get_or_create(&self.account_id);
let bucket = state.buckets.get(physical_id)?;
match attribute {
"Arn" => Some(format!("arn:aws:s3:::{}", bucket.name)),
"DomainName" => Some(format!("{}.s3.amazonaws.com", bucket.name)),
"RegionalDomainName" => {
Some(format!("{}.s3.{}.amazonaws.com", bucket.name, self.region))
}
"DualStackDomainName" => Some(format!(
"{}.s3.dualstack.{}.amazonaws.com",
bucket.name, self.region
)),
"WebsiteURL" => Some(format!(
"http://{}.s3-website-{}.amazonaws.com",
bucket.name, self.region
)),
_ => None,
}
}
fn get_att_lambda_function(&self, physical_id: &str, attribute: &str) -> Option<String> {
let function_name = parse_lambda_function_name(physical_id);
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
match attribute {
"Arn" => state
.functions
.get(&function_name)
.map(|f| f.function_arn.clone()),
"FunctionUrl" => state
.function_url_configs
.get(&function_name)
.map(|u| u.function_url.clone()),
"Version" => state
.functions
.get(&function_name)
.map(|f| f.version.clone()),
_ => None,
}
}
fn get_att_iam_role(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
let role = state
.roles
.values()
.find(|r| r.arn == physical_id || r.role_name == physical_id)?;
match attribute {
"Arn" => Some(role.arn.clone()),
"RoleId" => Some(role.role_id.clone()),
_ => None,
}
}
fn get_att_sqs_queue(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.sqs_state.write();
let state = accounts.get_or_create(&self.account_id);
let queue = state.queues.get(physical_id)?;
match attribute {
"Arn" => Some(queue.arn.clone()),
"QueueName" => Some(queue.queue_name.clone()),
"QueueUrl" => Some(queue.queue_url.clone()),
_ => None,
}
}
fn get_att_sns_topic(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.sns_state.write();
let state = accounts.get_or_create(&self.account_id);
let topic = state.topics.get(physical_id)?;
match attribute {
"TopicArn" => Some(topic.topic_arn.clone()),
"TopicName" => Some(topic.name.clone()),
_ => None,
}
}
fn get_att_dynamodb_table(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.dynamodb_state.write();
let state = accounts.get_or_create(&self.account_id);
let table = state.tables.get(physical_id)?;
match attribute {
"Arn" => Some(table.arn.clone()),
"StreamArn" => table.stream_arn.clone(),
_ => None,
}
}
fn get_att_kms_key(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.kms_state.write();
let state = accounts.get_or_create(&self.account_id);
let key = state.keys.get(physical_id)?;
match attribute {
"Arn" => Some(key.arn.clone()),
"KeyId" => Some(key.key_id.clone()),
_ => None,
}
}
fn get_att_secrets_manager_secret(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
let secret = state.secrets.get(physical_id)?;
match attribute {
"Arn" | "Id" => Some(secret.arn.clone()),
_ => None,
}
}
fn get_att_cf_distribution(&self, physical_id: &str, attribute: &str) -> Option<String> {
let accounts = self.cloudfront_state.read();
let state = accounts.get("000000000000")?;
let dist = state.distributions.get(physical_id)?;
match attribute {
"DomainName" => Some(dist.domain_name.clone()),
"Id" => Some(dist.id.clone()),
_ => None,
}
}
pub fn delete_resource(&self, resource: &StackResource) -> Result<(), String> {
match resource.resource_type.as_str() {
"AWS::SQS::Queue" => self.delete_sqs_queue(&resource.physical_id),
"AWS::SNS::Topic" => self.delete_sns_topic(&resource.physical_id),
"AWS::SNS::Subscription" => self.delete_sns_subscription(&resource.physical_id),
"AWS::SSM::Parameter" => self.delete_ssm_parameter(&resource.physical_id),
"AWS::IAM::Role" => self.delete_iam_role(&resource.physical_id),
"AWS::IAM::Policy" => self.delete_iam_policy(&resource.physical_id),
"AWS::IAM::User" => self.delete_iam_user(&resource.physical_id),
"AWS::IAM::Group" => self.delete_iam_group(&resource.physical_id),
"AWS::IAM::ManagedPolicy" => self.delete_iam_managed_policy(&resource.physical_id),
"AWS::IAM::UserToGroupAddition" => {
self.delete_iam_user_to_group_addition(&resource.physical_id)
}
"AWS::IAM::AccessKey" => self.delete_iam_access_key(&resource.physical_id),
"AWS::IAM::InstanceProfile" => self.delete_iam_instance_profile(&resource.physical_id),
"AWS::IAM::OIDCProvider" => self.delete_iam_oidc_provider(&resource.physical_id),
"AWS::IAM::SAMLProvider" => self.delete_iam_saml_provider(&resource.physical_id),
"AWS::IAM::ServiceLinkedRole" => {
self.delete_iam_service_linked_role(&resource.physical_id)
}
"AWS::IAM::VirtualMFADevice" => {
self.delete_iam_virtual_mfa_device(&resource.physical_id)
}
"AWS::S3::Bucket" => self.delete_s3_bucket(&resource.physical_id),
"AWS::Events::Rule" => self.delete_eventbridge_rule(&resource.physical_id),
"AWS::Events::Connection" => self.delete_eventbridge_connection(&resource.physical_id),
"AWS::Events::EventBus" => self.delete_eventbridge_event_bus(&resource.physical_id),
"AWS::Events::EventBusPolicy" => {
self.delete_eventbridge_event_bus_policy(&resource.physical_id)
}
"AWS::Events::Endpoint" => self.delete_eventbridge_endpoint(&resource.physical_id),
"AWS::Events::ApiDestination" => {
self.delete_eventbridge_api_destination(&resource.physical_id)
}
"AWS::Events::Archive" => self.delete_eventbridge_archive(&resource.physical_id),
"AWS::DynamoDB::Table" => self.delete_dynamodb_table(&resource.physical_id),
"AWS::Logs::LogGroup" => self.delete_log_group(&resource.physical_id),
"AWS::Logs::LogStream" => self.delete_log_stream(&resource.physical_id),
"AWS::Logs::MetricFilter" => self.delete_metric_filter(&resource.physical_id),
"AWS::Logs::SubscriptionFilter" => {
self.delete_subscription_filter(&resource.physical_id)
}
"AWS::Logs::Destination" => self.delete_logs_destination(&resource.physical_id),
"AWS::Logs::ResourcePolicy" => self.delete_logs_resource_policy(&resource.physical_id),
"AWS::Logs::QueryDefinition" => {
self.delete_logs_query_definition(&resource.physical_id)
}
"AWS::Logs::Delivery" => self.delete_logs_delivery(&resource.physical_id),
"AWS::Logs::DeliveryDestination" => {
self.delete_logs_delivery_destination(&resource.physical_id)
}
"AWS::Logs::DeliverySource" => self.delete_logs_delivery_source(&resource.physical_id),
"AWS::Lambda::Function" => self.delete_lambda_function(&resource.physical_id),
"AWS::Lambda::Permission" => self.delete_lambda_permission(&resource.physical_id),
"AWS::Lambda::EventSourceMapping" => {
self.delete_lambda_event_source_mapping(&resource.physical_id)
}
"AWS::Lambda::LayerVersion" => self.delete_lambda_layer_version(&resource.physical_id),
"AWS::Lambda::Url" => self.delete_lambda_url(&resource.physical_id),
"AWS::Lambda::Alias" => self.delete_lambda_alias(&resource.physical_id),
"AWS::Lambda::Version" => self.delete_lambda_version(&resource.physical_id),
"AWS::SecretsManager::Secret" => {
self.delete_secrets_manager_secret(&resource.physical_id)
}
"AWS::Kinesis::Stream" => self.delete_kinesis_stream(&resource.physical_id),
"AWS::Kinesis::StreamConsumer" => {
self.delete_kinesis_stream_consumer(&resource.physical_id)
}
"AWS::KMS::Key" => self.delete_kms_key(&resource.physical_id),
"AWS::KMS::ReplicaKey" => self.delete_kms_replica_key(&resource.physical_id),
"AWS::KMS::Alias" => self.delete_kms_alias(&resource.physical_id),
"AWS::ECR::Repository" => self.delete_ecr_repository(&resource.physical_id),
"AWS::ECR::RepositoryPolicy" => {
self.delete_ecr_repository_policy(&resource.physical_id)
}
"AWS::ECR::LifecyclePolicy" => self.delete_ecr_lifecycle_policy(&resource.physical_id),
"AWS::ECR::RegistryPolicy" => self.delete_ecr_registry_policy(),
"AWS::ECR::ReplicationConfiguration" => self.delete_ecr_replication_configuration(),
"AWS::ECR::RegistryScanningConfiguration" => {
self.delete_ecr_registry_scanning_configuration()
}
"AWS::ECR::PullThroughCacheRule" => {
self.delete_ecr_pull_through_cache_rule(&resource.physical_id)
}
"AWS::CloudWatch::Alarm" => self.delete_cloudwatch_alarm(&resource.physical_id),
"AWS::CloudWatch::Dashboard" => self.delete_cloudwatch_dashboard(&resource.physical_id),
"AWS::ElasticLoadBalancingV2::LoadBalancer" => {
self.delete_elbv2_load_balancer(&resource.physical_id)
}
"AWS::ElasticLoadBalancingV2::TargetGroup" => {
self.delete_elbv2_target_group(&resource.physical_id)
}
"AWS::ElasticLoadBalancingV2::Listener" => {
self.delete_elbv2_listener(&resource.physical_id)
}
"AWS::ElasticLoadBalancingV2::ListenerRule" => {
self.delete_elbv2_listener_rule(&resource.physical_id)
}
"AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
self.delete_elbv2_listener_certificate(&resource.physical_id)
}
"AWS::ElasticLoadBalancingV2::TrustStore" => {
self.delete_elbv2_trust_store(&resource.physical_id)
}
"AWS::Organizations::Organization" => self.delete_organization(&resource.physical_id),
"AWS::Organizations::OrganizationalUnit" => {
self.delete_organization_unit(&resource.physical_id)
}
"AWS::Organizations::Account" => {
self.delete_organization_account(&resource.physical_id)
}
"AWS::Organizations::Policy" => self.delete_organization_policy(&resource.physical_id),
"AWS::Organizations::ResourcePolicy" => {
self.delete_organization_resource_policy(&resource.physical_id)
}
"AWS::Cognito::UserPool" => self.delete_cognito_user_pool(&resource.physical_id),
"AWS::Cognito::UserPoolClient" => {
self.delete_cognito_user_pool_client(&resource.physical_id)
}
"AWS::Cognito::UserPoolDomain" => {
self.delete_cognito_user_pool_domain(&resource.physical_id)
}
"AWS::Cognito::IdentityPool" => {
self.delete_cognito_identity_pool(&resource.physical_id)
}
"AWS::Cognito::IdentityPoolRoleAttachment" => {
self.delete_cognito_identity_pool_role_attachment(&resource.physical_id)
}
"AWS::RDS::DBSubnetGroup" => self.delete_rds_subnet_group(&resource.physical_id),
"AWS::RDS::DBParameterGroup" => self.delete_rds_parameter_group(&resource.physical_id),
"AWS::RDS::DBClusterParameterGroup" => {
self.delete_rds_cluster_parameter_group(&resource.physical_id)
}
"AWS::RDS::OptionGroup" => self.delete_rds_option_group(&resource.physical_id),
"AWS::RDS::EventSubscription" => {
self.delete_rds_event_subscription(&resource.physical_id)
}
"AWS::RDS::DBSecurityGroup" => self.delete_rds_security_group(&resource.physical_id),
"AWS::RDS::DBProxy" => self.delete_rds_db_proxy(&resource.physical_id),
"AWS::RDS::DBInstance" => self.delete_rds_db_instance(&resource.physical_id),
"AWS::RDS::DBCluster" => self.delete_rds_db_cluster(&resource.physical_id),
"AWS::ECS::Cluster" => self.delete_ecs_cluster(&resource.physical_id),
"AWS::ECS::TaskDefinition" => self.delete_ecs_task_definition(&resource.physical_id),
"AWS::ECS::Service" => self.delete_ecs_service(&resource.physical_id),
"AWS::ECS::CapacityProvider" => {
self.delete_ecs_capacity_provider(&resource.physical_id)
}
"AWS::CertificateManager::Certificate" => {
self.delete_acm_certificate(&resource.physical_id)
}
"AWS::CertificateManager::Account" => self.delete_acm_account(),
"AWS::ElastiCache::ParameterGroup" => {
self.delete_ec_parameter_group(&resource.physical_id)
}
"AWS::ElastiCache::SubnetGroup" => self.delete_ec_subnet_group(&resource.physical_id),
"AWS::ElastiCache::SecurityGroup" => {
self.delete_ec_security_group(&resource.physical_id)
}
"AWS::ElastiCache::User" => self.delete_ec_user(&resource.physical_id),
"AWS::ElastiCache::UserGroup" => self.delete_ec_user_group(&resource.physical_id),
"AWS::ElastiCache::CacheCluster" => self.delete_ec_cache_cluster(&resource.physical_id),
"AWS::ElastiCache::ReplicationGroup" => {
self.delete_ec_replication_group(&resource.physical_id)
}
"AWS::Route53::HostedZone" => self.delete_route53_hosted_zone(&resource.physical_id),
"AWS::Route53::RecordSet" => {
self.delete_route53_record_set(&resource.physical_id, &resource.attributes)
}
"AWS::Route53::HealthCheck" => self.delete_route53_health_check(&resource.physical_id),
"AWS::Route53::DNSSEC" => self.delete_route53_dnssec(&resource.physical_id),
"AWS::Route53::KeySigningKey" => {
self.delete_route53_key_signing_key(&resource.physical_id)
}
"AWS::CloudFront::CloudFrontOriginAccessIdentity" => {
self.delete_cf_origin_access_identity(&resource.physical_id)
}
"AWS::CloudFront::Distribution" => self.delete_cf_distribution(&resource.physical_id),
"AWS::CloudFront::OriginAccessControl" => {
self.delete_cf_origin_access_control(&resource.physical_id)
}
"AWS::CloudFront::PublicKey" => self.delete_cf_public_key(&resource.physical_id),
"AWS::CloudFront::KeyGroup" => self.delete_cf_key_group(&resource.physical_id),
"AWS::CloudFront::Function" => self.delete_cf_function(&resource.physical_id),
"AWS::CloudFront::CachePolicy" => self.delete_cf_cache_policy(&resource.physical_id),
"AWS::CloudFront::OriginRequestPolicy" => {
self.delete_cf_origin_request_policy(&resource.physical_id)
}
"AWS::CloudFront::ResponseHeadersPolicy" => {
self.delete_cf_response_headers_policy(&resource.physical_id)
}
"AWS::StepFunctions::StateMachine" => {
self.delete_sfn_state_machine(&resource.physical_id)
}
"AWS::StepFunctions::Activity" => self.delete_sfn_activity(&resource.physical_id),
"AWS::StepFunctions::StateMachineVersion" => {
self.delete_sfn_version(&resource.physical_id)
}
"AWS::StepFunctions::StateMachineAlias" => self.delete_sfn_alias(&resource.physical_id),
"AWS::WAFv2::WebACL" => self.delete_wafv2_web_acl(&resource.physical_id),
"AWS::WAFv2::IPSet" => self.delete_wafv2_ip_set(&resource.physical_id),
"AWS::WAFv2::RegexPatternSet" => {
self.delete_wafv2_regex_pattern_set(&resource.physical_id)
}
"AWS::WAFv2::RuleGroup" => self.delete_wafv2_rule_group(&resource.physical_id),
"AWS::WAFv2::LoggingConfiguration" => {
self.delete_wafv2_logging_configuration(&resource.physical_id)
}
"AWS::WAFv2::WebACLAssociation" => {
self.delete_wafv2_web_acl_association(&resource.physical_id)
}
"AWS::ApiGateway::RestApi" => self.delete_apigw_rest_api(&resource.physical_id),
"AWS::ApiGateway::Resource" => {
self.delete_apigw_resource(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGateway::Method" => self.delete_apigw_method(&resource.physical_id),
"AWS::ApiGateway::Deployment" => {
self.delete_apigw_deployment(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGateway::Stage" => {
self.delete_apigw_stage(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGateway::Authorizer" => {
self.delete_apigw_authorizer(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGateway::RequestValidator" => {
self.delete_apigw_request_validator(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGateway::Model" => {
self.delete_apigw_model(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGateway::GatewayResponse" => {
self.delete_apigw_gateway_response(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGateway::UsagePlan" => self.delete_apigw_usage_plan(&resource.physical_id),
"AWS::ApiGateway::ApiKey" => self.delete_apigw_api_key(&resource.physical_id),
"AWS::ApiGateway::UsagePlanKey" => {
self.delete_apigw_usage_plan_key(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGateway::DomainName" => self.delete_apigw_domain_name(&resource.physical_id),
"AWS::ApiGateway::BasePathMapping" => {
self.delete_apigw_base_path_mapping(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGatewayV2::Api" => self.delete_apigwv2_api(&resource.physical_id),
"AWS::ApiGatewayV2::Route" => {
self.delete_apigwv2_route(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGatewayV2::Integration" => {
self.delete_apigwv2_integration(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGatewayV2::IntegrationResponse" => self
.delete_apigwv2_integration_response(&resource.physical_id, &resource.attributes),
"AWS::ApiGatewayV2::RouteResponse" => {
self.delete_apigwv2_route_response(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGatewayV2::Stage" => {
self.delete_apigwv2_stage(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGatewayV2::Deployment" => {
self.delete_apigwv2_deployment(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGatewayV2::Authorizer" => {
self.delete_apigwv2_authorizer(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGatewayV2::DomainName" => {
self.delete_apigwv2_domain_name(&resource.physical_id)
}
"AWS::ApiGatewayV2::ApiMapping" => {
self.delete_apigwv2_api_mapping(&resource.physical_id, &resource.attributes)
}
"AWS::ApiGatewayV2::VpcLink" => self.delete_apigwv2_vpc_link(&resource.physical_id),
"AWS::ApiGatewayV2::Model" => {
self.delete_apigwv2_model(&resource.physical_id, &resource.attributes)
}
"AWS::SES::ConfigurationSet" => {
self.delete_ses_configuration_set(&resource.physical_id)
}
"AWS::SES::ConfigurationSetEventDestination" => {
self.delete_ses_event_destination(&resource.physical_id, &resource.attributes)
}
"AWS::SES::EmailIdentity" => self.delete_ses_email_identity(&resource.physical_id),
"AWS::SES::Template" => self.delete_ses_template(&resource.physical_id),
"AWS::SES::ContactList" => self.delete_ses_contact_list(&resource.physical_id),
"AWS::SES::DedicatedIpPool" => self.delete_ses_dedicated_ip_pool(&resource.physical_id),
"AWS::SES::ReceiptRule" => {
self.delete_ses_receipt_rule(&resource.physical_id, &resource.attributes)
}
"AWS::SES::ReceiptRuleSet" => self.delete_ses_receipt_rule_set(&resource.physical_id),
"AWS::SES::ReceiptFilter" => self.delete_ses_receipt_filter(&resource.physical_id),
"AWS::SES::VdmAttributes" => Ok(()),
"AWS::SecretsManager::RotationSchedule" => {
self.delete_secrets_manager_rotation_schedule(&resource.physical_id)
}
"AWS::SecretsManager::ResourcePolicy" => {
self.delete_secrets_manager_resource_policy(&resource.physical_id)
}
"AWS::SecretsManager::SecretTargetAttachment" => Ok(()),
"AWS::ApplicationAutoScaling::ScalableTarget" => self
.delete_application_autoscaling_scalable_target(
&resource.physical_id,
&resource.attributes,
),
"AWS::ApplicationAutoScaling::ScalingPolicy" => self
.delete_application_autoscaling_scaling_policy(
&resource.physical_id,
&resource.attributes,
),
"AWS::Athena::DataCatalog" => self.delete_athena_data_catalog(&resource.physical_id),
"AWS::Athena::NamedQuery" => self.delete_athena_named_query(&resource.physical_id),
"AWS::Athena::WorkGroup" => self.delete_athena_work_group(&resource.physical_id),
"AWS::Athena::PreparedStatement" => {
self.delete_athena_prepared_statement(&resource.physical_id, &resource.attributes)
}
"AWS::KinesisFirehose::DeliveryStream" => {
self.delete_firehose_delivery_stream(&resource.physical_id)
}
"AWS::Glue::Database" => self.delete_glue_database(&resource.physical_id),
"AWS::CloudFormation::Stack" => self.delete_cloudformation_stack(&resource.physical_id),
"AWS::Glue::Table" => self.delete_glue_table(&resource.physical_id),
"AWS::Glue::Partition" => {
self.delete_glue_partition(&resource.physical_id, &resource.attributes)
}
t if t.starts_with("Custom::") || t == "AWS::CloudFormation::CustomResource" => {
self.delete_custom_resource(resource)
}
other => Err(format!("Unsupported resource type: {other}")),
}
}
fn create_sqs_queue(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let queue_name = props
.get("QueueName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id);
let mut __sqs_mas = self.sqs_state.write();
let state = __sqs_mas.get_or_create(&self.account_id);
let queue_url = format!("{}/{}/{}", state.endpoint, state.account_id, queue_name);
let arn = format!(
"arn:aws:sqs:{}:{}:{}",
state.region, state.account_id, queue_name
);
let is_fifo = queue_name.ends_with(".fifo");
let mut attributes = std::collections::BTreeMap::new();
if let Some(obj) = props.as_object() {
for (k, v) in obj {
if k != "QueueName" {
if let Some(s) = v.as_str() {
attributes.insert(k.clone(), s.to_string());
} else if let Some(n) = v.as_i64() {
attributes.insert(k.clone(), n.to_string());
}
}
}
}
let queue = SqsQueue {
queue_name: queue_name.to_string(),
queue_url: queue_url.clone(),
arn: arn.clone(),
created_at: Utc::now(),
messages: std::collections::VecDeque::new(),
inflight: Vec::new(),
attributes,
is_fifo,
dedup_cache: std::collections::BTreeMap::new(),
redrive_policy: None,
tags: std::collections::BTreeMap::new(),
next_sequence_number: 0,
permission_labels: Vec::new(),
receipt_handle_map: std::collections::BTreeMap::new(),
};
state
.name_to_url
.insert(queue_name.to_string(), queue_url.clone());
state.queues.insert(queue_url.clone(), queue);
Ok(ProvisionResult::new(queue_url.clone())
.with("Arn", arn)
.with("QueueName", queue_name)
.with("QueueUrl", queue_url))
}
fn delete_sqs_queue(&self, physical_id: &str) -> Result<(), String> {
let mut __sqs_mas = self.sqs_state.write();
let state = __sqs_mas.get_or_create(&self.account_id);
if let Some(queue) = state.queues.remove(physical_id) {
state.name_to_url.remove(&queue.queue_name);
}
Ok(())
}
fn create_sns_topic(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let topic_name = props
.get("TopicName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id);
let mut __sns_mas = self.sns_state.write();
let state = __sns_mas.get_or_create(&self.account_id);
let topic_arn = format!(
"arn:aws:sns:{}:{}:{}",
state.region, state.account_id, topic_name
);
let topic = SnsTopic {
topic_arn: topic_arn.clone(),
name: topic_name.to_string(),
attributes: BTreeMap::new(),
tags: Vec::new(),
is_fifo: topic_name.ends_with(".fifo"),
created_at: Utc::now(),
subscriptions_deleted: 0,
};
state.topics.insert(topic_arn.clone(), topic);
Ok(ProvisionResult::new(topic_arn.clone())
.with("TopicArn", topic_arn)
.with("TopicName", topic_name))
}
fn delete_sns_topic(&self, physical_id: &str) -> Result<(), String> {
let mut __sns_mas = self.sns_state.write();
let state = __sns_mas.get_or_create(&self.account_id);
state.topics.remove(physical_id);
state
.subscriptions
.retain(|_, sub| sub.topic_arn != physical_id);
Ok(())
}
fn create_sns_subscription(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let topic_arn = props
.get("TopicArn")
.and_then(|v| v.as_str())
.ok_or("SNS Subscription requires TopicArn")?;
let protocol = props
.get("Protocol")
.and_then(|v| v.as_str())
.ok_or("SNS Subscription requires Protocol")?;
let endpoint = props
.get("Endpoint")
.and_then(|v| v.as_str())
.ok_or("SNS Subscription requires Endpoint")?;
let mut __sns_mas = self.sns_state.write();
let state = __sns_mas.get_or_create(&self.account_id);
if !state.topics.contains_key(topic_arn) {
return Err(format!("Topic ARN does not exist: {topic_arn}"));
}
let sub_arn = format!("{}:{}", topic_arn, Uuid::new_v4());
let subscription = SnsSubscription {
subscription_arn: sub_arn.clone(),
topic_arn: topic_arn.to_string(),
protocol: protocol.to_string(),
endpoint: endpoint.to_string(),
owner: state.account_id.clone(),
attributes: BTreeMap::new(),
confirmed: true,
confirmation_token: None,
};
state.subscriptions.insert(sub_arn.clone(), subscription);
Ok(ProvisionResult::new(sub_arn.clone()).with("Arn", sub_arn))
}
fn delete_sns_subscription(&self, physical_id: &str) -> Result<(), String> {
let mut __sns_mas = self.sns_state.write();
let state = __sns_mas.get_or_create(&self.account_id);
state.subscriptions.remove(physical_id);
Ok(())
}
fn create_ssm_parameter(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("SSM Parameter requires Name")?;
let value = props
.get("Value")
.and_then(|v| v.as_str())
.ok_or("SSM Parameter requires Value")?;
let param_type = props
.get("Type")
.and_then(|v| v.as_str())
.unwrap_or("String");
let mut accounts = self.ssm_state.write();
let state = accounts.get_or_create(&self.account_id);
let arn = format!(
"arn:aws:ssm:{}:{}:parameter{}",
self.region,
self.account_id,
if name.starts_with('/') {
name.to_string()
} else {
format!("/{name}")
}
);
let parameter = SsmParameter {
name: name.to_string(),
value: value.to_string(),
param_type: param_type.to_string(),
version: 1,
arn: arn.clone(),
last_modified: Utc::now(),
history: Vec::new(),
tags: BTreeMap::new(),
labels: BTreeMap::new(),
description: props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
allowed_pattern: None,
key_id: None,
data_type: "text".to_string(),
tier: "Standard".to_string(),
policies: None,
expiration_notified: false,
no_change_notified: false,
};
state.parameters.insert(name.to_string(), parameter);
Ok(ProvisionResult::new(name)
.with("Type", param_type)
.with("Value", value))
}
fn delete_ssm_parameter(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ssm_state.write();
let state = accounts.get_or_create(&self.account_id);
state.parameters.remove(physical_id);
Ok(())
}
fn create_iam_role(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let role_name = props
.get("RoleName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id);
let assume_role_policy = props
.get("AssumeRolePolicyDocument")
.map(|v| {
if v.is_string() {
v.as_str().unwrap().to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.unwrap_or_default();
let path = props.get("Path").and_then(|v| v.as_str()).unwrap_or("/");
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
let role_id = format!(
"FKIA{}",
&Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
);
let arn = format!(
"arn:aws:iam::{}:role{}{}",
state.account_id,
if path == "/" { "/" } else { path },
role_name
);
let role = IamRole {
role_name: role_name.to_string(),
role_id: role_id.clone(),
arn: arn.clone(),
path: path.to_string(),
assume_role_policy_document: assume_role_policy,
created_at: Utc::now(),
description: props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
max_session_duration: 3600,
tags: Vec::new(),
permissions_boundary: None,
};
state.roles.insert(role_name.to_string(), role);
Ok(ProvisionResult::new(arn.clone())
.with("Arn", arn)
.with("RoleId", role_id))
}
fn delete_iam_role(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
let role_name = state
.roles
.iter()
.find(|(_, r)| r.arn == physical_id)
.map(|(name, _)| name.clone());
if let Some(name) = role_name {
state.roles.remove(&name);
state.role_policies.remove(&name);
state.role_inline_policies.remove(&name);
}
Ok(())
}
fn create_iam_policy(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let policy_name = props
.get("PolicyName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id);
let policy_document = props
.get("PolicyDocument")
.map(|v| {
if v.is_string() {
v.as_str().unwrap().to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.unwrap_or_default();
let path = props.get("Path").and_then(|v| v.as_str()).unwrap_or("/");
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
let policy_id = format!(
"FSIA{}",
&Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
);
let arn = format!(
"arn:aws:iam::{}:policy{}{}",
state.account_id,
if path == "/" { "/" } else { path },
policy_name
);
let now = Utc::now();
let policy = IamPolicy {
policy_name: policy_name.to_string(),
policy_id,
arn: arn.clone(),
path: path.to_string(),
description: props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
created_at: now,
tags: Vec::new(),
default_version_id: "v1".to_string(),
versions: vec![PolicyVersion {
version_id: "v1".to_string(),
document: policy_document,
is_default: true,
created_at: now,
}],
next_version_num: 2,
attachment_count: 0,
};
state.policies.insert(arn.clone(), policy);
Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
}
fn delete_iam_policy(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state.policies.remove(physical_id);
Ok(())
}
fn create_iam_user(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let user_name = props
.get("UserName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let path = props
.get("Path")
.and_then(|v| v.as_str())
.unwrap_or("/")
.to_string();
let permissions_boundary = props
.get("PermissionsBoundary")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let tags = parse_iam_tags(props.get("Tags"));
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
if state.users.contains_key(&user_name) {
return Err(format!("User {user_name} already exists"));
}
let arn = format!(
"arn:aws:iam::{}:user{}{}",
state.account_id, path, user_name
);
let user_id = format!(
"AIDA{}",
&Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
);
let user = IamUser {
user_name: user_name.clone(),
user_id: user_id.clone(),
arn: arn.clone(),
path,
created_at: Utc::now(),
tags,
permissions_boundary,
};
state.users.insert(user_name.clone(), user);
if let Some(policies) = props.get("Policies").and_then(|v| v.as_array()) {
let inline = state
.user_inline_policies
.entry(user_name.clone())
.or_default();
for p in policies {
if let (Some(n), Some(doc)) = (
p.get("PolicyName").and_then(|v| v.as_str()),
p.get("PolicyDocument"),
) {
let document = if doc.is_string() {
doc.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(doc).unwrap_or_default()
};
inline.insert(n.to_string(), document);
}
}
}
if let Some(arns) = props.get("ManagedPolicyArns").and_then(|v| v.as_array()) {
let attached = state.user_policies.entry(user_name.clone()).or_default();
for a in arns {
if let Some(s) = a.as_str() {
if !attached.contains(&s.to_string()) {
attached.push(s.to_string());
}
}
}
}
if let Some(groups) = props.get("Groups").and_then(|v| v.as_array()) {
for g in groups {
if let Some(g_name) = g.as_str() {
if let Some(group) = state.groups.get_mut(g_name) {
if !group.members.iter().any(|m| m == &user_name) {
group.members.push(user_name.clone());
}
}
}
}
}
Ok(ProvisionResult::new(user_name).with("Arn", arn))
}
fn delete_iam_user(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state.users.remove(physical_id);
state.user_inline_policies.remove(physical_id);
state.user_policies.remove(physical_id);
state.access_keys.remove(physical_id);
for group in state.groups.values_mut() {
group.members.retain(|m| m != physical_id);
}
Ok(())
}
fn create_iam_group(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let group_name = props
.get("GroupName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let path = props
.get("Path")
.and_then(|v| v.as_str())
.unwrap_or("/")
.to_string();
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
if state.groups.contains_key(&group_name) {
return Err(format!("Group {group_name} already exists"));
}
let arn = format!(
"arn:aws:iam::{}:group{}{}",
state.account_id, path, group_name
);
let group_id = format!(
"AGPA{}",
&Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
);
let mut inline_policies: BTreeMap<String, String> = BTreeMap::new();
if let Some(policies) = props.get("Policies").and_then(|v| v.as_array()) {
for p in policies {
if let (Some(n), Some(doc)) = (
p.get("PolicyName").and_then(|v| v.as_str()),
p.get("PolicyDocument"),
) {
let document = if doc.is_string() {
doc.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(doc).unwrap_or_default()
};
inline_policies.insert(n.to_string(), document);
}
}
}
let mut attached_policies: Vec<String> = Vec::new();
if let Some(arns) = props.get("ManagedPolicyArns").and_then(|v| v.as_array()) {
for a in arns {
if let Some(s) = a.as_str() {
attached_policies.push(s.to_string());
}
}
}
state.groups.insert(
group_name.clone(),
IamGroup {
group_name: group_name.clone(),
group_id,
arn: arn.clone(),
path,
created_at: Utc::now(),
members: Vec::new(),
inline_policies,
attached_policies,
},
);
Ok(ProvisionResult::new(group_name).with("Arn", arn))
}
fn delete_iam_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state.groups.remove(physical_id);
Ok(())
}
fn create_iam_managed_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let policy_name = props
.get("ManagedPolicyName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let policy_document = props
.get("PolicyDocument")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.unwrap_or_default();
let path = props
.get("Path")
.and_then(|v| v.as_str())
.unwrap_or("/")
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
let arn = format!(
"arn:aws:iam::{}:policy{}{}",
state.account_id,
if path == "/" { "/" } else { path.as_str() },
policy_name
);
if state.policies.contains_key(&arn) {
return Err(format!("Managed policy {policy_name} already exists"));
}
let policy_id = format!(
"ANPA{}",
&Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
);
let now = Utc::now();
state.policies.insert(
arn.clone(),
IamPolicy {
policy_name,
policy_id,
arn: arn.clone(),
path,
description,
created_at: now,
tags: Vec::new(),
default_version_id: "v1".to_string(),
versions: vec![PolicyVersion {
version_id: "v1".to_string(),
document: policy_document,
is_default: true,
created_at: now,
}],
next_version_num: 2,
attachment_count: 0,
},
);
if let Some(users) = props.get("Users").and_then(|v| v.as_array()) {
for u in users {
if let Some(name) = u.as_str() {
let attached = state.user_policies.entry(name.to_string()).or_default();
if !attached.contains(&arn) {
attached.push(arn.clone());
}
}
}
}
if let Some(groups) = props.get("Groups").and_then(|v| v.as_array()) {
for g in groups {
if let Some(name) = g.as_str() {
if let Some(group) = state.groups.get_mut(name) {
if !group.attached_policies.contains(&arn) {
group.attached_policies.push(arn.clone());
}
}
}
}
}
if let Some(roles) = props.get("Roles").and_then(|v| v.as_array()) {
for r in roles {
if let Some(name) = r.as_str() {
let attached = state.role_policies.entry(name.to_string()).or_default();
if !attached.contains(&arn) {
attached.push(arn.clone());
}
}
}
}
Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
}
fn delete_iam_managed_policy(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state.policies.remove(physical_id);
for arns in state.user_policies.values_mut() {
arns.retain(|a| a != physical_id);
}
for arns in state.role_policies.values_mut() {
arns.retain(|a| a != physical_id);
}
for group in state.groups.values_mut() {
group.attached_policies.retain(|a| a != physical_id);
}
Ok(())
}
fn create_iam_user_to_group_addition(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let group_name = props
.get("GroupName")
.and_then(|v| v.as_str())
.ok_or_else(|| "GroupName is required".to_string())?
.to_string();
let users: Vec<String> = props
.get("Users")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|u| u.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
let group = state
.groups
.get_mut(&group_name)
.ok_or_else(|| format!("Group {group_name} does not exist"))?;
for u in &users {
if !group.members.iter().any(|m| m == u) {
group.members.push(u.clone());
}
}
let physical_id = format!("{group_name}|{}", users.join(","));
Ok(ProvisionResult::new(physical_id))
}
fn delete_iam_user_to_group_addition(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some((group_name, users)) = physical_id.split_once('|') {
if let Some(group) = state.groups.get_mut(group_name) {
let to_remove: Vec<&str> = users.split(',').filter(|s| !s.is_empty()).collect();
group.members.retain(|m| !to_remove.iter().any(|u| u == m));
}
}
Ok(())
}
fn create_iam_access_key(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let user_name = props
.get("UserName")
.and_then(|v| v.as_str())
.ok_or_else(|| "UserName is required".to_string())?
.to_string();
let status = props
.get("Status")
.and_then(|v| v.as_str())
.unwrap_or("Active")
.to_string();
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.users.contains_key(&user_name) {
return Err(format!("User {user_name} does not exist"));
}
let access_key_id = format!(
"AKIA{}",
&Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
);
let secret_access_key: String = Uuid::new_v4()
.to_string()
.replace('-', "")
.chars()
.take(40)
.collect();
state
.access_keys
.entry(user_name.clone())
.or_default()
.push(IamAccessKey {
access_key_id: access_key_id.clone(),
secret_access_key: secret_access_key.clone(),
user_name: user_name.clone(),
status,
created_at: Utc::now(),
});
Ok(ProvisionResult::new(access_key_id.clone()).with("SecretAccessKey", secret_access_key))
}
fn delete_iam_access_key(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
for keys in state.access_keys.values_mut() {
keys.retain(|k| k.access_key_id != physical_id);
}
Ok(())
}
fn create_iam_instance_profile(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("InstanceProfileName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let path = props
.get("Path")
.and_then(|v| v.as_str())
.unwrap_or("/")
.to_string();
let roles: Vec<String> = props
.get("Roles")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|r| r.as_str())
.map(|s| {
if let Some(rest) = s.strip_prefix("arn:aws:iam::") {
rest.split(":role/")
.nth(1)
.map(|name| name.to_string())
.unwrap_or_else(|| s.to_string())
} else {
s.to_string()
}
})
.collect()
})
.unwrap_or_default();
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
if state.instance_profiles.contains_key(&name) {
return Err(format!("InstanceProfile {name} already exists"));
}
for role_name in &roles {
if !state.roles.contains_key(role_name) {
return Err(format!(
"InstanceProfile {name}: referenced role {role_name} not yet provisioned"
));
}
}
let arn = format!(
"arn:aws:iam::{}:instance-profile{}{}",
state.account_id, path, name
);
let id = format!(
"AIPA{}",
&Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
);
state.instance_profiles.insert(
name.clone(),
IamInstanceProfile {
instance_profile_name: name.clone(),
instance_profile_id: id,
arn: arn.clone(),
path,
created_at: Utc::now(),
roles,
tags: Vec::new(),
},
);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_iam_instance_profile(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state.instance_profiles.remove(physical_id);
Ok(())
}
fn create_iam_oidc_provider(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let url = props
.get("Url")
.and_then(|v| v.as_str())
.ok_or("Url is required")?
.to_string();
let client_id_list: Vec<String> = props
.get("ClientIdList")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let thumbprint_list: Vec<String> = props
.get("ThumbprintList")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let url_path = url
.trim_start_matches("https://")
.trim_start_matches("http://")
.to_string();
let arn = format!(
"arn:aws:iam::{}:oidc-provider/{}",
self.account_id, url_path
);
let provider = OidcProvider {
arn: arn.clone(),
url,
client_id_list,
thumbprint_list,
created_at: Utc::now(),
tags: Vec::new(),
};
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state.oidc_providers.insert(arn.clone(), provider);
Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
}
fn delete_iam_oidc_provider(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state.oidc_providers.remove(physical_id);
Ok(())
}
fn create_iam_saml_provider(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| {
let suffix = Uuid::new_v4().simple().to_string();
format!("{}-{}", resource.logical_id, &suffix[..8])
});
let saml_metadata_document = props
.get("SamlMetadataDocument")
.and_then(|v| v.as_str())
.ok_or("SamlMetadataDocument is required")?
.to_string();
let arn = format!("arn:aws:iam::{}:saml-provider/{name}", self.account_id);
let now = Utc::now();
let valid_until = now + chrono::Duration::days(365 * 10);
let provider = SamlProvider {
arn: arn.clone(),
name,
saml_metadata_document,
created_at: now,
valid_until,
tags: Vec::new(),
};
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state.saml_providers.insert(arn.clone(), provider);
Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
}
fn delete_iam_saml_provider(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state.saml_providers.remove(physical_id);
Ok(())
}
fn create_iam_service_linked_role(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let aws_service_name = props
.get("AWSServiceName")
.and_then(|v| v.as_str())
.ok_or("AWSServiceName is required")?
.to_string();
let custom_suffix = props
.get("CustomSuffix")
.and_then(|v| v.as_str())
.map(String::from);
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let service_short = aws_service_name.split('.').next().unwrap_or("Service");
let role_name = match &custom_suffix {
Some(s) => format!("AWSServiceRoleFor{service_short}_{s}"),
None => format!("AWSServiceRoleFor{service_short}"),
};
let path = format!("/aws-service-role/{aws_service_name}/");
let arn = format!("arn:aws:iam::{}:role{path}{role_name}", self.account_id);
let assume_role_policy_document = serde_json::json!({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": aws_service_name.clone()},
"Action": "sts:AssumeRole"
}]
})
.to_string();
let role_id_suffix = Uuid::new_v4().simple().to_string();
let role = IamRole {
role_name: role_name.clone(),
role_id: format!("AROA{}", role_id_suffix[..16].to_uppercase()),
arn: arn.clone(),
path,
assume_role_policy_document,
created_at: Utc::now(),
description: Some(description),
max_session_duration: 3600,
tags: Vec::new(),
permissions_boundary: None,
};
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state.roles.insert(role_name.clone(), role);
Ok(ProvisionResult::new(role_name)
.with("Arn", arn)
.with("RoleId", String::new()))
}
fn delete_iam_service_linked_role(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state.roles.remove(physical_id);
Ok(())
}
fn create_iam_virtual_mfa_device(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("VirtualMfaDeviceName")
.and_then(|v| v.as_str())
.ok_or("VirtualMfaDeviceName is required")?
.to_string();
let path = props
.get("Path")
.and_then(|v| v.as_str())
.unwrap_or("/")
.to_string();
let serial_number = format!("arn:aws:iam::{}:mfa{}{name}", self.account_id, path);
let seed = format!("BASE32SEED{}", Uuid::new_v4().simple());
let user = props
.get("Users")
.and_then(|v| v.as_array())
.and_then(|arr| arr.first())
.and_then(|v| v.as_str())
.map(String::from);
let device = VirtualMfaDevice {
serial_number: serial_number.clone(),
base32_string_seed: seed,
qr_code_png: String::new(),
enable_date: user.as_ref().map(|_| Utc::now()),
user,
tags: Vec::new(),
};
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state
.virtual_mfa_devices
.insert(serial_number.clone(), device);
Ok(ProvisionResult::new(serial_number.clone()).with("SerialNumber", serial_number))
}
fn delete_iam_virtual_mfa_device(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.iam_state.write();
let state = accounts.get_or_create(&self.account_id);
state.virtual_mfa_devices.remove(physical_id);
Ok(())
}
fn create_s3_bucket(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let bucket_name = props
.get("BucketName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id);
let mut __s3_mas = self.s3_state.write();
let state = __s3_mas.get_or_create(&self.account_id);
let region = state.region.clone();
let bucket = S3Bucket::new(bucket_name, &state.region, &state.account_id);
state.buckets.insert(bucket_name.to_string(), bucket);
let arn = format!("arn:aws:s3:::{bucket_name}");
let domain_name = format!("{bucket_name}.s3.amazonaws.com");
let regional_domain_name = format!("{bucket_name}.s3.{region}.amazonaws.com");
let dual_stack_domain_name = format!("{bucket_name}.s3.dualstack.{region}.amazonaws.com");
let website_url = format!("http://{bucket_name}.s3-website-{region}.amazonaws.com");
Ok(ProvisionResult::new(bucket_name)
.with("Arn", arn)
.with("DomainName", domain_name)
.with("RegionalDomainName", regional_domain_name)
.with("DualStackDomainName", dual_stack_domain_name)
.with("WebsiteURL", website_url))
}
fn delete_s3_bucket(&self, physical_id: &str) -> Result<(), String> {
let mut __s3_mas = self.s3_state.write();
let state = __s3_mas.get_or_create(&self.account_id);
state.buckets.remove(physical_id);
Ok(())
}
fn create_eventbridge_rule(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rule_name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id);
let event_bus_name = props
.get("EventBusName")
.and_then(|v| v.as_str())
.unwrap_or("default");
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
if !state.buses.contains_key(event_bus_name) {
return Err(format!("Event bus does not exist: {event_bus_name}"));
}
let arn = if event_bus_name == "default" {
format!(
"arn:aws:events:{}:{}:rule/{}",
state.region, state.account_id, rule_name
)
} else {
format!(
"arn:aws:events:{}:{}:rule/{}/{}",
state.region, state.account_id, event_bus_name, rule_name
)
};
let rule = EventRule {
name: rule_name.to_string(),
arn: arn.clone(),
event_bus_name: event_bus_name.to_string(),
event_pattern: props.get("EventPattern").map(|v| {
if v.is_string() {
v.as_str().unwrap().to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
}),
schedule_expression: props
.get("ScheduleExpression")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
state: props
.get("State")
.and_then(|v| v.as_str())
.unwrap_or("ENABLED")
.to_string(),
description: props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
role_arn: props
.get("RoleArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
managed_by: None,
created_by: None,
targets: Vec::new(),
tags: std::collections::BTreeMap::new(),
last_fired: None,
};
state
.rules
.insert((event_bus_name.to_string(), rule_name.to_string()), rule);
Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
}
fn delete_eventbridge_rule(&self, physical_id: &str) -> Result<(), String> {
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.default_mut();
let key = state
.rules
.iter()
.find(|(_, r)| r.arn == physical_id)
.map(|(k, _)| k.clone());
if let Some(k) = key {
state.rules.remove(&k);
}
Ok(())
}
fn create_eventbridge_connection(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let authorization_type = props
.get("AuthorizationType")
.and_then(|v| v.as_str())
.unwrap_or("API_KEY")
.to_string();
let auth_parameters = props
.get("AuthParameters")
.cloned()
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
if state.connections.contains_key(&name) {
return Err(format!("Connection {name} already exists"));
}
let now = Utc::now();
let arn = format!(
"arn:aws:events:{}:{}:connection/{}/{}",
state.region,
state.account_id,
name,
Uuid::new_v4().as_simple()
);
let secret_arn = format!(
"arn:aws:secretsmanager:{}:{}:secret:events!connection/{}-{}",
state.region,
state.account_id,
name,
Uuid::new_v4().as_simple()
);
let connection = Connection {
name: name.clone(),
arn: arn.clone(),
description,
authorization_type,
auth_parameters,
connection_state: "AUTHORIZED".to_string(),
secret_arn: secret_arn.clone(),
creation_time: now,
last_modified_time: now,
last_authorized_time: now,
};
state.connections.insert(name.clone(), connection);
Ok(ProvisionResult::new(name)
.with("Arn", arn)
.with("SecretArn", secret_arn))
}
fn delete_eventbridge_connection(&self, physical_id: &str) -> Result<(), String> {
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
state.connections.remove(physical_id);
Ok(())
}
fn create_eventbridge_api_destination(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let connection_arn = props
.get("ConnectionArn")
.and_then(|v| v.as_str())
.ok_or_else(|| "ConnectionArn is required".to_string())?
.to_string();
let invocation_endpoint = props
.get("InvocationEndpoint")
.and_then(|v| v.as_str())
.ok_or_else(|| "InvocationEndpoint is required".to_string())?
.to_string();
let http_method = props
.get("HttpMethod")
.and_then(|v| v.as_str())
.unwrap_or("POST")
.to_string();
let invocation_rate_limit_per_second = props
.get("InvocationRateLimitPerSecond")
.and_then(|v| v.as_i64());
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
if state.api_destinations.contains_key(&name) {
return Err(format!("ApiDestination {name} already exists"));
}
let now = Utc::now();
let arn = format!(
"arn:aws:events:{}:{}:api-destination/{}/{}",
state.region,
state.account_id,
name,
Uuid::new_v4().as_simple()
);
state.api_destinations.insert(
name.clone(),
ApiDestination {
name: name.clone(),
arn: arn.clone(),
description,
connection_arn,
invocation_endpoint,
http_method,
invocation_rate_limit_per_second,
state: "ACTIVE".to_string(),
creation_time: now,
last_modified_time: now,
},
);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_eventbridge_api_destination(&self, physical_id: &str) -> Result<(), String> {
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
state.api_destinations.remove(physical_id);
Ok(())
}
fn create_eventbridge_archive(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("ArchiveName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let event_source_arn = props
.get("SourceArn")
.and_then(|v| v.as_str())
.ok_or_else(|| "SourceArn is required".to_string())?
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let event_pattern = props.get("EventPattern").map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
});
let retention_days = props
.get("RetentionDays")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
if state.archives.contains_key(&name) {
return Err(format!("Archive {name} already exists"));
}
let arn = format!(
"arn:aws:events:{}:{}:archive/{}",
state.region, state.account_id, name
);
state.archives.insert(
name.clone(),
Archive {
name: name.clone(),
arn: arn.clone(),
event_source_arn,
description,
event_pattern,
retention_days,
state: "ENABLED".to_string(),
creation_time: Utc::now(),
event_count: 0,
size_bytes: 0,
events: Vec::new(),
},
);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_eventbridge_archive(&self, physical_id: &str) -> Result<(), String> {
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
state.archives.remove(physical_id);
Ok(())
}
fn create_eventbridge_event_bus(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from);
let kms_key_identifier = props
.get("KmsKeyIdentifier")
.and_then(|v| v.as_str())
.map(String::from);
let dead_letter_config = props.get("DeadLetterConfig").cloned();
let policy = props.get("Policy").cloned();
let arn = format!(
"arn:aws:events:{}:{}:event-bus/{name}",
self.region, self.account_id
);
let now = Utc::now();
let bus = EventBus {
name: name.clone(),
arn: arn.clone(),
tags: BTreeMap::new(),
policy,
description,
kms_key_identifier,
dead_letter_config,
creation_time: now,
last_modified_time: now,
};
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
state.buses.insert(name.clone(), bus);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_eventbridge_event_bus(&self, physical_id: &str) -> Result<(), String> {
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
if physical_id == "default" {
return Ok(());
}
state.buses.remove(physical_id);
Ok(())
}
fn create_eventbridge_event_bus_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let bus_name = props
.get("EventBusName")
.and_then(|v| v.as_str())
.unwrap_or("default")
.to_string();
let statement = if let Some(s) = props.get("Statement") {
s.clone()
} else {
let sid = props
.get("Sid")
.or_else(|| props.get("StatementId"))
.and_then(|v| v.as_str())
.map(String::from);
let action = props
.get("Action")
.and_then(|v| v.as_str())
.map(String::from);
let principal = props.get("Principal").cloned();
let condition = props.get("Condition").cloned();
let mut obj = serde_json::json!({
"Effect": "Allow",
"Resource": format!(
"arn:aws:events:{}:{}:event-bus/{bus_name}",
self.region, self.account_id
),
});
if let (Some(sid), Some(obj)) = (sid, obj.as_object_mut()) {
obj.insert("Sid".to_string(), serde_json::Value::String(sid));
}
if let (Some(action), Some(obj)) = (action, obj.as_object_mut()) {
obj.insert("Action".to_string(), serde_json::Value::String(action));
}
if let (Some(principal), Some(obj)) = (principal, obj.as_object_mut()) {
obj.insert("Principal".to_string(), principal);
}
if let (Some(condition), Some(obj)) = (condition, obj.as_object_mut()) {
obj.insert("Condition".to_string(), condition);
}
obj
};
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
let bus = state
.buses
.get_mut(&bus_name)
.ok_or_else(|| format!("EventBus {bus_name} not yet provisioned"))?;
match bus.policy.as_mut() {
Some(serde_json::Value::Object(obj)) => {
if let Some(serde_json::Value::Array(arr)) = obj.get_mut("Statement") {
arr.push(statement);
} else {
obj.insert(
"Statement".to_string(),
serde_json::Value::Array(vec![statement]),
);
}
}
_ => {
bus.policy = Some(serde_json::json!({
"Version": "2012-10-17",
"Statement": [statement],
}));
}
}
let pid = format!("{bus_name}|{}", Uuid::new_v4().simple());
Ok(ProvisionResult::new(pid))
}
fn delete_eventbridge_event_bus_policy(&self, physical_id: &str) -> Result<(), String> {
let bus_name = physical_id
.split_once('|')
.map(|(b, _)| b.to_string())
.unwrap_or_else(|| "default".to_string());
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
if let Some(bus) = state.buses.get_mut(&bus_name) {
bus.policy = None;
}
Ok(())
}
fn create_eventbridge_endpoint(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from);
let routing_config = props
.get("RoutingConfig")
.cloned()
.ok_or("RoutingConfig is required")?;
let replication_config = props.get("ReplicationConfig").cloned();
let event_buses = props
.get("EventBuses")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let role_arn = props
.get("RoleArn")
.and_then(|v| v.as_str())
.map(String::from);
let endpoint_id = Uuid::new_v4().simple().to_string()[..16].to_string();
let arn = format!(
"arn:aws:events:{}:{}:endpoint/{name}",
self.region, self.account_id
);
let endpoint_url = format!("https://{endpoint_id}.endpoint.events.amazonaws.com");
let now = Utc::now();
let endpoint = Endpoint {
name: name.clone(),
arn: arn.clone(),
endpoint_id: endpoint_id.clone(),
endpoint_url: Some(endpoint_url.clone()),
description,
routing_config,
replication_config,
event_buses,
role_arn,
state: "ACTIVE".to_string(),
creation_time: now,
last_modified_time: now,
};
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
state.endpoints.insert(name.clone(), endpoint);
Ok(ProvisionResult::new(name)
.with("Arn", arn)
.with("EndpointId", endpoint_id)
.with("EndpointUrl", endpoint_url)
.with("State", "ACTIVE"))
}
fn delete_eventbridge_endpoint(&self, physical_id: &str) -> Result<(), String> {
let mut eb_accounts = self.eventbridge_state.write();
let state = eb_accounts.get_or_create(&self.account_id);
state.endpoints.remove(physical_id);
Ok(())
}
fn create_dynamodb_table(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let table_name = props
.get("TableName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id);
let mut key_schema = Vec::new();
if let Some(ks) = props.get("KeySchema").and_then(|v| v.as_array()) {
for item in ks {
let attr_name = item
.get("AttributeName")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let key_type = item
.get("KeyType")
.and_then(|v| v.as_str())
.unwrap_or("HASH")
.to_string();
key_schema.push(KeySchemaElement {
attribute_name: attr_name,
key_type,
});
}
}
let mut attribute_definitions = Vec::new();
if let Some(defs) = props.get("AttributeDefinitions").and_then(|v| v.as_array()) {
for item in defs {
let attr_name = item
.get("AttributeName")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let attr_type = item
.get("AttributeType")
.and_then(|v| v.as_str())
.unwrap_or("S")
.to_string();
attribute_definitions.push(AttributeDefinition {
attribute_name: attr_name,
attribute_type: attr_type,
});
}
}
let billing_mode = props
.get("BillingMode")
.and_then(|v| v.as_str())
.unwrap_or("PAY_PER_REQUEST")
.to_string();
let provisioned_throughput = if billing_mode == "PROVISIONED" {
if let Some(pt) = props.get("ProvisionedThroughput") {
ProvisionedThroughput {
read_capacity_units: pt
.get("ReadCapacityUnits")
.and_then(|v| v.as_i64())
.unwrap_or(5),
write_capacity_units: pt
.get("WriteCapacityUnits")
.and_then(|v| v.as_i64())
.unwrap_or(5),
}
} else {
ProvisionedThroughput {
read_capacity_units: 5,
write_capacity_units: 5,
}
}
} else {
ProvisionedThroughput {
read_capacity_units: 0,
write_capacity_units: 0,
}
};
let (stream_enabled, stream_view_type) =
if let Some(stream_spec) = props.get("StreamSpecification") {
let view_type = stream_spec
.get("StreamViewType")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let enabled = stream_spec
.get("StreamEnabled")
.and_then(|v| v.as_bool().or_else(|| v.as_str().map(|s| s == "true")))
.unwrap_or(view_type.is_some());
(enabled, view_type)
} else {
(false, None)
};
let deletion_protection_enabled = props
.get("DeletionProtectionEnabled")
.and_then(|v| v.as_bool().or_else(|| v.as_str().map(|s| s == "true")))
.unwrap_or(false);
let on_demand_throughput = props
.get("OnDemandThroughput")
.map(|odt| OnDemandThroughput {
max_read_request_units: odt
.get("MaxReadRequestUnits")
.and_then(|v| v.as_i64())
.unwrap_or(-1),
max_write_request_units: odt
.get("MaxWriteRequestUnits")
.and_then(|v| v.as_i64())
.unwrap_or(-1),
});
let mut __ddb_mas = self.dynamodb_state.write();
let state = __ddb_mas.get_or_create(&self.account_id);
let arn = format!(
"arn:aws:dynamodb:{}:{}:table/{}",
state.region, state.account_id, table_name
);
let stream_arn = if stream_enabled {
Some(format!(
"{}/stream/{}",
arn,
Utc::now().format("%Y-%m-%dT%H:%M:%S.%3f")
))
} else {
None
};
let stream_arn_attr = stream_arn.clone();
let table = DynamoTable {
name: table_name.to_string(),
arn: arn.clone(),
table_id: Uuid::new_v4().to_string().replace('-', ""),
key_schema,
attribute_definitions,
provisioned_throughput,
items: Vec::new(),
gsi: Vec::new(),
lsi: Vec::new(),
tags: BTreeMap::new(),
created_at: Utc::now(),
status: "ACTIVE".to_string(),
item_count: 0,
size_bytes: 0,
billing_mode,
ttl_attribute: None,
ttl_enabled: false,
resource_policy: None,
pitr_enabled: false,
kinesis_destinations: Vec::new(),
contributor_insights_status: "DISABLED".to_string(),
contributor_insights_counters: BTreeMap::new(),
stream_enabled,
stream_view_type,
stream_arn,
stream_records: Arc::new(RwLock::new(Vec::new())),
sse_type: None,
sse_kms_key_arn: None,
deletion_protection_enabled,
on_demand_throughput,
};
state.tables.insert(table_name.to_string(), table);
let mut result = ProvisionResult::new(arn.clone()).with("Arn", arn);
if let Some(stream_arn_value) = stream_arn_attr {
result = result.with("StreamArn", stream_arn_value);
}
Ok(result)
}
fn delete_dynamodb_table(&self, physical_id: &str) -> Result<(), String> {
let mut __ddb_mas = self.dynamodb_state.write();
let state = __ddb_mas.get_or_create(&self.account_id);
let table_name = state
.tables
.iter()
.find(|(_, t)| t.arn == physical_id)
.map(|(name, _)| name.clone());
if let Some(name) = table_name {
state.tables.remove(&name);
}
Ok(())
}
fn create_log_group(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let log_group_name = props
.get("LogGroupName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id);
let retention_in_days = props
.get("RetentionInDays")
.and_then(|v| v.as_i64())
.map(|v| v as i32);
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
let arn = format!(
"arn:aws:logs:{}:{}:log-group:{}:*",
state.region, state.account_id, log_group_name
);
let log_group = fakecloud_logs::LogGroup {
name: log_group_name.to_string(),
arn: arn.clone(),
creation_time: Utc::now().timestamp_millis(),
retention_in_days,
kms_key_id: None,
stored_bytes: 0,
log_streams: std::collections::BTreeMap::new(),
tags: std::collections::BTreeMap::new(),
subscription_filters: Vec::new(),
data_protection_policy: None,
index_policies: Vec::new(),
transformer: None,
deletion_protection: false,
log_group_class: Some("STANDARD".to_string()),
};
state
.log_groups
.insert(log_group_name.to_string(), log_group);
Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
}
fn create_lambda_function(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let function_name = props
.get("FunctionName")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| {
format!(
"{}-{}-{}",
self.stack_id
.rsplit('/')
.nth(1)
.unwrap_or(&resource.logical_id),
resource.logical_id,
Uuid::new_v4()
.to_string()
.split('-')
.next()
.unwrap_or("rand")
)
});
let cfg = parse_lambda_function_props(props)?;
let function_arn = format!(
"arn:aws:lambda:{}:{}:function:{}",
self.region, self.account_id, function_name
);
let code_zip = if cfg.code_zip.is_some() {
cfg.code_zip.clone()
} else if let (Some(bucket_name), Some(key)) = (&cfg.s3_bucket, &cfg.s3_key) {
Some(self.read_s3_object_bytes(bucket_name, key).map_err(|e| {
format!("Failed to read Code.S3Bucket={bucket_name} Code.S3Key={key}: {e}")
})?)
} else {
None
};
let (code_sha256, code_size) = match &code_zip {
Some(bytes) => (sha256_b64(bytes), bytes.len() as i64),
None => (String::new(), 0),
};
let layers: Vec<AttachedLayer> = {
let accounts = self.lambda_state.read();
cfg.layers
.iter()
.map(|arn| AttachedLayer {
arn: arn.clone(),
code_size: layer_code_size(&accounts, arn),
})
.collect()
};
let func = fakecloud_lambda::LambdaFunction {
function_name: function_name.clone(),
function_arn: function_arn.clone(),
runtime: cfg.runtime,
role: cfg.role,
handler: cfg.handler,
description: cfg.description,
timeout: cfg.timeout,
memory_size: cfg.memory_size,
code_sha256,
code_size,
version: "$LATEST".to_string(),
last_modified: Utc::now(),
tags: cfg.tags,
environment: cfg.environment,
architectures: cfg.architectures,
package_type: cfg.package_type,
code_zip,
image_uri: cfg.image_uri,
policy: None,
layers,
revision_id: Uuid::new_v4().to_string(),
tracing_mode: cfg.tracing_mode,
kms_key_arn: cfg.kms_key_arn,
ephemeral_storage_size: cfg.ephemeral_storage_size,
vpc_config: cfg.vpc_config,
snap_start: cfg.snap_start,
dead_letter_config_arn: cfg.dead_letter_config_arn,
file_system_configs: cfg.file_system_configs,
logging_config: cfg.logging_config,
image_config: None,
signing_profile_version_arn: None,
signing_job_arn: None,
runtime_version_config: None,
master_arn: None,
state_reason: None,
state_reason_code: None,
last_update_status_reason: None,
last_update_status_reason_code: None,
};
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
state.functions.insert(function_name.clone(), func);
Ok(ProvisionResult::new(function_name.clone())
.with("Arn", function_arn)
.with("FunctionName", function_name)
.with("Version", "$LATEST"))
}
fn update_lambda_function(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let function_name = existing.physical_id.clone();
let cfg = parse_lambda_function_props(props)?;
let new_code_zip = if cfg.code_zip.is_some() {
cfg.code_zip.clone()
} else if let (Some(bucket_name), Some(key)) = (&cfg.s3_bucket, &cfg.s3_key) {
Some(self.read_s3_object_bytes(bucket_name, key).map_err(|e| {
format!("Failed to read Code.S3Bucket={bucket_name} Code.S3Key={key}: {e}")
})?)
} else {
None
};
let resolved_layers: Vec<AttachedLayer> = {
let accounts = self.lambda_state.read();
cfg.layers
.iter()
.map(|arn| AttachedLayer {
arn: arn.clone(),
code_size: layer_code_size(&accounts, arn),
})
.collect()
};
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
let func = state.functions.get_mut(&function_name).ok_or_else(|| {
format!("Cannot update {function_name}: function does not exist in lambda state")
})?;
func.runtime = cfg.runtime;
func.role = cfg.role;
func.handler = cfg.handler;
func.description = cfg.description;
func.timeout = cfg.timeout;
func.memory_size = cfg.memory_size;
func.environment = cfg.environment;
func.architectures = cfg.architectures;
func.package_type = cfg.package_type;
func.tracing_mode = cfg.tracing_mode;
func.kms_key_arn = cfg.kms_key_arn;
func.ephemeral_storage_size = cfg.ephemeral_storage_size;
func.vpc_config = cfg.vpc_config;
func.snap_start = cfg.snap_start;
func.dead_letter_config_arn = cfg.dead_letter_config_arn;
func.file_system_configs = cfg.file_system_configs;
func.logging_config = cfg.logging_config;
func.layers = resolved_layers;
if cfg.image_uri.is_some() {
func.image_uri = cfg.image_uri;
}
if !cfg.tags.is_empty() {
func.tags = cfg.tags;
}
if let Some(bytes) = new_code_zip {
func.code_sha256 = sha256_b64(&bytes);
func.code_size = bytes.len() as i64;
func.code_zip = Some(bytes);
}
func.last_modified = Utc::now();
func.revision_id = Uuid::new_v4().to_string();
let function_arn = func.function_arn.clone();
Ok(ProvisionResult::new(function_name.clone())
.with("Arn", function_arn)
.with("FunctionName", function_name)
.with("Version", "$LATEST"))
}
fn delete_lambda_function(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.lambda_state.write();
let state = accounts.default_mut();
state.functions.remove(physical_id);
Ok(())
}
fn read_s3_object_bytes(&self, bucket: &str, key: &str) -> Result<Vec<u8>, String> {
let mut accounts = self.s3_state.write();
let state = accounts.get_or_create(&self.account_id);
let body_ref = {
let b = state
.buckets
.get(bucket)
.ok_or_else(|| format!("S3 bucket {bucket} does not exist"))?;
let object = b
.objects
.get(key)
.ok_or_else(|| format!("S3 object s3://{bucket}/{key} does not exist"))?;
object.body.clone()
};
state
.read_body(&body_ref)
.map(|b| b.to_vec())
.map_err(|e| format!("S3 read failed: {e}"))
}
fn create_lambda_permission(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let function_name = parse_lambda_function_name(
props
.get("FunctionName")
.and_then(|v| v.as_str())
.ok_or_else(|| "FunctionName is required".to_string())?,
);
let statement_id = format!(
"cfn-{}-{}",
resource.logical_id,
&Uuid::new_v4().simple().to_string()[..8]
);
self.append_lambda_permission_statement(&function_name, &statement_id, props)?;
let physical_id = format!("{function_name}|{statement_id}");
Ok(ProvisionResult::new(physical_id).with("Id", statement_id))
}
fn append_lambda_permission_statement(
&self,
function_name: &str,
statement_id: &str,
props: &serde_json::Value,
) -> Result<String, String> {
let action = props
.get("Action")
.and_then(|v| v.as_str())
.ok_or_else(|| "Action is required".to_string())?
.to_string();
let principal = props
.get("Principal")
.and_then(|v| v.as_str())
.ok_or_else(|| "Principal is required".to_string())?
.to_string();
let source_arn = props
.get("SourceArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let source_account = props
.get("SourceAccount")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let event_source_token = props
.get("EventSourceToken")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let function_url_auth_type = props
.get("FunctionUrlAuthType")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let principal_org_id = props
.get("PrincipalOrgID")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
let func = state.functions.get_mut(function_name).ok_or_else(|| {
format!(
"Function {function_name} does not exist yet — retry once it has been provisioned"
)
})?;
let mut doc: serde_json::Value = func
.policy
.as_deref()
.and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
.filter(|v| v.is_object())
.unwrap_or_else(|| serde_json::json!({"Version": "2012-10-17", "Statement": []}));
if !doc.get("Statement").map(|s| s.is_array()).unwrap_or(false) {
doc["Statement"] = serde_json::json!([]);
}
let principal_value =
if principal.ends_with(".amazonaws.com") || principal.contains(".amazon") {
serde_json::json!({ "Service": principal })
} else {
serde_json::json!({ "AWS": principal })
};
let mut arn_like = serde_json::Map::new();
let mut string_equals = serde_json::Map::new();
if let Some(src) = source_arn {
arn_like.insert("AWS:SourceArn".to_string(), serde_json::Value::String(src));
}
if let Some(acct) = source_account {
string_equals.insert(
"AWS:SourceAccount".to_string(),
serde_json::Value::String(acct),
);
}
if let Some(token) = event_source_token {
string_equals.insert(
"lambda:EventSourceToken".to_string(),
serde_json::Value::String(token),
);
}
if let Some(auth) = function_url_auth_type {
string_equals.insert(
"lambda:FunctionUrlAuthType".to_string(),
serde_json::Value::String(auth),
);
}
if let Some(org) = principal_org_id {
string_equals.insert(
"aws:PrincipalOrgID".to_string(),
serde_json::Value::String(org),
);
}
let mut conditions = serde_json::Map::new();
if !arn_like.is_empty() {
conditions.insert("ArnLike".to_string(), serde_json::Value::Object(arn_like));
}
if !string_equals.is_empty() {
conditions.insert(
"StringEquals".to_string(),
serde_json::Value::Object(string_equals),
);
}
let mut statement = serde_json::Map::new();
statement.insert(
"Sid".to_string(),
serde_json::Value::String(statement_id.to_string()),
);
statement.insert(
"Effect".to_string(),
serde_json::Value::String("Allow".to_string()),
);
statement.insert("Principal".to_string(), principal_value);
statement.insert("Action".to_string(), serde_json::Value::String(action));
statement.insert(
"Resource".to_string(),
serde_json::Value::String(func.function_arn.clone()),
);
if !conditions.is_empty() {
statement.insert(
"Condition".to_string(),
serde_json::Value::Object(conditions),
);
}
doc["Statement"]
.as_array_mut()
.unwrap()
.push(serde_json::Value::Object(statement));
func.policy = Some(doc.to_string());
Ok(func.function_arn.clone())
}
fn update_lambda_permission(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let Some((function_name, statement_id)) = existing.physical_id.split_once('|') else {
return Err(format!(
"Permission physical id `{}` is malformed; expected `{{function}}|{{sid}}`",
existing.physical_id
));
};
{
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(func) = state.functions.get_mut(function_name) {
if let Some(policy_str) = func.policy.as_deref() {
if let Ok(mut doc) = serde_json::from_str::<serde_json::Value>(policy_str) {
if let Some(arr) = doc.get_mut("Statement").and_then(|v| v.as_array_mut()) {
arr.retain(|s| {
s.get("Sid").and_then(|v| v.as_str()) != Some(statement_id)
});
func.policy = Some(doc.to_string());
}
}
}
}
}
self.append_lambda_permission_statement(function_name, statement_id, &resource.properties)?;
Ok(ProvisionResult::new(existing.physical_id.clone()).with("Id", statement_id.to_string()))
}
fn delete_lambda_permission(&self, physical_id: &str) -> Result<(), String> {
let Some((function_name, sid)) = physical_id.split_once('|') else {
return Ok(());
};
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(func) = state.functions.get_mut(function_name) {
if let Some(policy_str) = func.policy.as_deref() {
if let Ok(mut doc) = serde_json::from_str::<serde_json::Value>(policy_str) {
if let Some(arr) = doc.get_mut("Statement").and_then(|v| v.as_array_mut()) {
arr.retain(|s| s.get("Sid").and_then(|v| v.as_str()) != Some(sid));
func.policy = Some(doc.to_string());
}
}
}
}
Ok(())
}
fn create_lambda_event_source_mapping(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let function_name = parse_lambda_function_name(
props
.get("FunctionName")
.and_then(|v| v.as_str())
.ok_or_else(|| "FunctionName is required".to_string())?,
);
let cfg = parse_lambda_event_source_mapping_props(props)?;
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.functions.contains_key(&function_name) {
return Err(format!(
"Function {function_name} does not exist yet — retry once it has been provisioned"
));
}
let function_arn = format!(
"arn:aws:lambda:{}:{}:function:{}",
self.region, self.account_id, function_name
);
let uuid = Uuid::new_v4().to_string();
let esm = EventSourceMapping {
uuid: uuid.clone(),
function_arn,
event_source_arn: cfg.event_source_arn,
batch_size: cfg.batch_size,
enabled: cfg.enabled,
state: if cfg.enabled {
"Enabled".to_string()
} else {
"Disabled".to_string()
},
last_modified: Utc::now(),
filter_patterns: cfg.filter_patterns,
maximum_batching_window_in_seconds: cfg.maximum_batching_window_in_seconds,
starting_position: cfg.starting_position,
starting_position_timestamp: cfg.starting_position_timestamp,
parallelization_factor: cfg.parallelization_factor,
function_response_types: cfg.function_response_types,
kms_key_arn: cfg.kms_key_arn,
metrics_config: cfg.metrics_config,
destination_config: cfg.destination_config,
maximum_retry_attempts: cfg.maximum_retry_attempts,
maximum_record_age_in_seconds: cfg.maximum_record_age_in_seconds,
bisect_batch_on_function_error: cfg.bisect_batch_on_function_error,
tumbling_window_in_seconds: cfg.tumbling_window_in_seconds,
topics: cfg.topics,
queues: cfg.queues,
};
state.event_source_mappings.insert(uuid.clone(), esm);
Ok(ProvisionResult::new(uuid.clone()).with("Id", uuid))
}
fn update_lambda_event_source_mapping(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let cfg = parse_lambda_event_source_mapping_props(&resource.properties)?;
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
let esm = state
.event_source_mappings
.get_mut(&existing.physical_id)
.ok_or_else(|| {
format!(
"EventSourceMapping {} does not exist in lambda state",
existing.physical_id
)
})?;
esm.batch_size = cfg.batch_size;
esm.enabled = cfg.enabled;
esm.state = if cfg.enabled {
"Enabled".to_string()
} else {
"Disabled".to_string()
};
esm.last_modified = Utc::now();
esm.filter_patterns = cfg.filter_patterns;
esm.maximum_batching_window_in_seconds = cfg.maximum_batching_window_in_seconds;
esm.parallelization_factor = cfg.parallelization_factor;
esm.function_response_types = cfg.function_response_types;
esm.kms_key_arn = cfg.kms_key_arn;
esm.metrics_config = cfg.metrics_config;
esm.destination_config = cfg.destination_config;
esm.maximum_retry_attempts = cfg.maximum_retry_attempts;
esm.maximum_record_age_in_seconds = cfg.maximum_record_age_in_seconds;
esm.bisect_batch_on_function_error = cfg.bisect_batch_on_function_error;
esm.tumbling_window_in_seconds = cfg.tumbling_window_in_seconds;
Ok(ProvisionResult::new(existing.physical_id.clone())
.with("Id", existing.physical_id.clone()))
}
fn delete_lambda_event_source_mapping(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
state.event_source_mappings.remove(physical_id);
Ok(())
}
fn create_lambda_layer_version(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let layer_name = props
.get("LayerName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let license_info = props
.get("LicenseInfo")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let compatible_runtimes: Vec<String> = props
.get("CompatibleRuntimes")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let compatible_architectures: Vec<String> = props
.get("CompatibleArchitectures")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let content = props.get("Content");
let zip_bytes = if let Some(b64) = content
.and_then(|v| v.get("ZipFile"))
.and_then(|v| v.as_str())
{
use base64::Engine;
Some(
base64::engine::general_purpose::STANDARD
.decode(b64)
.map_err(|e| format!("Content.ZipFile is not valid base64: {e}"))?,
)
} else if let (Some(bucket), Some(key)) = (
content
.and_then(|c| c.get("S3Bucket"))
.and_then(|v| v.as_str()),
content
.and_then(|c| c.get("S3Key"))
.and_then(|v| v.as_str()),
) {
Some(self.read_s3_object_bytes(bucket, key).map_err(|e| {
format!("Failed to read Content.S3Bucket={bucket} Content.S3Key={key}: {e}")
})?)
} else {
None
};
let (code_sha256, code_size) = match zip_bytes.as_deref() {
Some(bytes) => (sha256_b64(bytes), bytes.len() as i64),
None => (String::new(), 0),
};
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
let layer_arn = format!(
"arn:aws:lambda:{}:{}:layer:{}",
self.region, self.account_id, layer_name
);
let layer = state
.layers
.entry(layer_name.clone())
.or_insert_with(|| Layer {
layer_name: layer_name.clone(),
layer_arn: layer_arn.clone(),
versions: Vec::new(),
});
let next_version = (layer.versions.len() as i64) + 1;
let version_arn = format!("{}:{}", layer.layer_arn, next_version);
layer.versions.push(LayerVersion {
version: next_version,
layer_version_arn: version_arn.clone(),
description: description.clone(),
created_date: Utc::now(),
compatible_runtimes,
license_info,
policy: None,
code_zip: zip_bytes,
code_sha256,
code_size,
compatible_architectures,
});
Ok(ProvisionResult::new(version_arn.clone())
.with("LayerVersionArn", version_arn)
.with("LayerArn", layer_arn))
}
fn delete_lambda_layer_version(&self, physical_id: &str) -> Result<(), String> {
let Some(idx) = physical_id.rfind(':') else {
return Ok(());
};
let (layer_arn, version_part) = physical_id.split_at(idx);
let version_part = &version_part[1..];
let Ok(version) = version_part.parse::<i64>() else {
return Ok(());
};
let layer_name = layer_arn.rsplit(':').next().unwrap_or("").to_string();
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(layer) = state.layers.get_mut(&layer_name) {
layer.versions.retain(|v| v.version != version);
if layer.versions.is_empty() {
state.layers.remove(&layer_name);
}
}
Ok(())
}
fn create_lambda_url(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let function_name = parse_lambda_function_name(
props
.get("TargetFunctionArn")
.and_then(|v| v.as_str())
.ok_or_else(|| "TargetFunctionArn is required".to_string())?,
);
let qualifier = props
.get("Qualifier")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let auth_type = props
.get("AuthType")
.and_then(|v| v.as_str())
.unwrap_or("NONE")
.to_string();
let invoke_mode = props
.get("InvokeMode")
.and_then(|v| v.as_str())
.unwrap_or("BUFFERED")
.to_string();
let cors = props.get("Cors").cloned();
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.functions.contains_key(&function_name) {
return Err(format!(
"Function {function_name} does not exist yet — retry once it has been provisioned"
));
}
let function_arn = match &qualifier {
Some(q) => format!(
"arn:aws:lambda:{}:{}:function:{}:{}",
self.region, self.account_id, function_name, q
),
None => format!(
"arn:aws:lambda:{}:{}:function:{}",
self.region, self.account_id, function_name
),
};
let function_url = format!("https://{function_name}.lambda-url.{}.on.aws/", self.region);
let now = Utc::now();
let cfg = FunctionUrlConfig {
function_arn: function_arn.clone(),
function_url: function_url.clone(),
auth_type,
cors,
creation_time: now,
last_modified_time: now,
invoke_mode,
};
let key = match &qualifier {
Some(q) => format!("{function_name}:{q}"),
None => function_name.clone(),
};
state.function_url_configs.insert(key.clone(), cfg);
Ok(ProvisionResult::new(key)
.with("FunctionArn", function_arn)
.with("FunctionUrl", function_url))
}
fn update_lambda_url(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let auth_type = props
.get("AuthType")
.and_then(|v| v.as_str())
.unwrap_or("NONE")
.to_string();
let invoke_mode = props
.get("InvokeMode")
.and_then(|v| v.as_str())
.unwrap_or("BUFFERED")
.to_string();
let cors = props.get("Cors").cloned();
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
let cfg = state
.function_url_configs
.get_mut(&existing.physical_id)
.ok_or_else(|| {
format!(
"FunctionUrlConfig {} does not exist in lambda state",
existing.physical_id
)
})?;
cfg.auth_type = auth_type;
cfg.invoke_mode = invoke_mode;
cfg.cors = cors;
cfg.last_modified_time = Utc::now();
let function_arn = cfg.function_arn.clone();
let function_url = cfg.function_url.clone();
Ok(ProvisionResult::new(existing.physical_id.clone())
.with("FunctionArn", function_arn)
.with("FunctionUrl", function_url))
}
fn delete_lambda_url(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
state.function_url_configs.remove(physical_id);
Ok(())
}
fn create_lambda_alias(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let function_name = parse_lambda_function_name(
props
.get("FunctionName")
.and_then(|v| v.as_str())
.ok_or_else(|| "FunctionName is required".to_string())?,
);
let alias_name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or_else(|| "Name is required".to_string())?
.to_string();
let function_version = props
.get("FunctionVersion")
.and_then(|v| v.as_str())
.unwrap_or("$LATEST")
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let routing_config = props.get("RoutingConfig").cloned();
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.functions.contains_key(&function_name) {
return Err(format!(
"Function {function_name} does not exist yet — retry once it has been provisioned"
));
}
let alias_arn = format!(
"arn:aws:lambda:{}:{}:function:{}:{}",
self.region, self.account_id, function_name, alias_name
);
let key = format!("{function_name}:{alias_name}");
state.aliases.insert(
key.clone(),
FunctionAlias {
alias_arn: alias_arn.clone(),
name: alias_name,
function_version,
description,
revision_id: Uuid::new_v4().to_string(),
routing_config,
},
);
if let Some(cnt) = props
.get("ProvisionedConcurrencyConfig")
.and_then(|v| v.get("ProvisionedConcurrentExecutions"))
.and_then(|v| v.as_i64())
{
state.provisioned_concurrency.insert(
key.clone(),
fakecloud_lambda::ProvisionedConcurrencyConfig {
requested: cnt,
allocated: cnt,
status: "READY".to_string(),
last_modified: Utc::now(),
},
);
}
Ok(ProvisionResult::new(key).with("AliasArn", alias_arn))
}
fn update_lambda_alias(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let function_version = props
.get("FunctionVersion")
.and_then(|v| v.as_str())
.unwrap_or("$LATEST")
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let routing_config = props.get("RoutingConfig").cloned();
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
let alias = state
.aliases
.get_mut(&existing.physical_id)
.ok_or_else(|| {
format!(
"Alias {} does not exist in lambda state",
existing.physical_id
)
})?;
alias.function_version = function_version;
alias.description = description;
alias.routing_config = routing_config;
alias.revision_id = Uuid::new_v4().to_string();
let alias_arn = alias.alias_arn.clone();
match props
.get("ProvisionedConcurrencyConfig")
.and_then(|v| v.get("ProvisionedConcurrentExecutions"))
.and_then(|v| v.as_i64())
{
Some(cnt) => {
state.provisioned_concurrency.insert(
existing.physical_id.clone(),
fakecloud_lambda::ProvisionedConcurrencyConfig {
requested: cnt,
allocated: cnt,
status: "READY".to_string(),
last_modified: Utc::now(),
},
);
}
None => {
state.provisioned_concurrency.remove(&existing.physical_id);
}
}
Ok(ProvisionResult::new(existing.physical_id.clone()).with("AliasArn", alias_arn))
}
fn delete_lambda_alias(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
state.aliases.remove(physical_id);
Ok(())
}
fn create_lambda_version(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let function_name = parse_lambda_function_name(
props
.get("FunctionName")
.and_then(|v| v.as_str())
.ok_or_else(|| "FunctionName is required".to_string())?,
);
let description_override = props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let expected_sha = props
.get("CodeSha256")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
let func = state
.functions
.get(&function_name)
.ok_or_else(|| format!("Function {function_name} does not exist yet — retry once it has been provisioned"))?
.clone();
if let Some(expected) = &expected_sha {
if !expected.is_empty() && expected != &func.code_sha256 {
return Err(format!(
"PreconditionFailed: CodeSha256 mismatch on {function_name} — expected {expected}, $LATEST is {actual}",
actual = func.code_sha256,
));
}
}
let versions = state
.function_versions
.entry(function_name.clone())
.or_default();
let next_version = (versions.len() as i64 + 1).to_string();
versions.push(next_version.clone());
let mut snapshot = func.clone();
snapshot.version = next_version.clone();
if let Some(desc) = description_override {
snapshot.description = desc;
}
state
.function_version_snapshots
.entry(function_name.clone())
.or_default()
.insert(next_version.clone(), snapshot);
let version_arn = format!(
"arn:aws:lambda:{}:{}:function:{}:{}",
self.region, self.account_id, function_name, next_version
);
let physical_id = format!("{function_name}:{next_version}");
Ok(ProvisionResult::new(physical_id)
.with("Version", next_version)
.with("FunctionArn", version_arn))
}
fn update_lambda_version(
&self,
existing: &StackResource,
_resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
let Some((function_name, version)) = existing.physical_id.split_once(':') else {
return Err(format!(
"Version physical id `{}` is malformed; expected `{{function}}:{{version}}`",
existing.physical_id
));
};
let exists = state
.function_version_snapshots
.get(function_name)
.map(|m| m.contains_key(version))
.unwrap_or(false);
if !exists {
return Err(format!(
"Version {version} for function {function_name} no longer exists in lambda state"
));
}
let version_arn = format!(
"arn:aws:lambda:{}:{}:function:{}:{}",
self.region, self.account_id, function_name, version
);
Ok(ProvisionResult::new(existing.physical_id.clone())
.with("Version", version.to_string())
.with("FunctionArn", version_arn))
}
fn update_lambda_layer_version(
&self,
existing: &StackResource,
_resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let arn = existing.physical_id.clone();
let layer_arn_only = arn
.rsplit_once(':')
.map(|(prefix, _)| prefix.to_string())
.unwrap_or_else(|| arn.clone());
Ok(ProvisionResult::new(existing.physical_id.clone())
.with("LayerVersionArn", arn)
.with("LayerArn", layer_arn_only))
}
fn delete_lambda_version(&self, physical_id: &str) -> Result<(), String> {
let Some((function_name, version)) = physical_id.split_once(':') else {
return Ok(());
};
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(versions) = state.function_versions.get_mut(function_name) {
versions.retain(|v| v != version);
}
if let Some(snapshots) = state.function_version_snapshots.get_mut(function_name) {
snapshots.remove(version);
}
Ok(())
}
fn create_secrets_manager_secret(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let kms_key_id = props
.get("KmsKeyId")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
let arn = format!(
"arn:aws:secretsmanager:{}:{}:secret:{}",
state.region, state.account_id, name
);
if state.secrets.contains_key(&arn) {
return Err(format!("Secret {name} already exists"));
}
let now = Utc::now();
let mut versions = BTreeMap::new();
let mut current_version_id: Option<String> = None;
let initial_string: Option<String> =
if let Some(secret_string) = props.get("SecretString").and_then(|v| v.as_str()) {
Some(secret_string.to_string())
} else if let Some(gen) = props.get("GenerateSecretString") {
Some(generate_secret_string_payload(gen)?)
} else {
None
};
if let Some(secret_string) = initial_string {
let version_id = Uuid::new_v4().to_string();
versions.insert(
version_id.clone(),
SecretVersion {
version_id: version_id.clone(),
secret_string: Some(secret_string),
secret_binary: None,
stages: vec!["AWSCURRENT".to_string()],
created_at: now,
},
);
current_version_id = Some(version_id);
}
let mut tags: Vec<(String, String)> = Vec::new();
if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
for t in arr {
if let (Some(k), Some(v)) = (
t.get("Key").and_then(|x| x.as_str()),
t.get("Value").and_then(|x| x.as_str()),
) {
tags.push((k.to_string(), v.to_string()));
}
}
}
let tags_set = !tags.is_empty();
let secret = Secret {
name: name.clone(),
arn: arn.clone(),
description,
kms_key_id,
versions,
current_version_id,
tags,
tags_ever_set: tags_set,
deleted: false,
deletion_date: None,
created_at: now,
last_changed_at: now,
last_accessed_at: None,
rotation_enabled: None,
rotation_lambda_arn: None,
rotation_rules: None,
last_rotated_at: None,
resource_policy: None,
};
state.secrets.insert(arn.clone(), secret);
Ok(ProvisionResult::new(arn.clone())
.with("Id", arn.clone())
.with("Name", name))
}
fn delete_secrets_manager_secret(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
state.secrets.remove(physical_id);
Ok(())
}
fn create_kinesis_stream(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let stream_name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let shard_count = props
.get("ShardCount")
.and_then(|v| v.as_i64())
.unwrap_or(1) as i32;
if shard_count <= 0 {
return Err("ShardCount must be greater than zero".to_string());
}
let stream_mode = props
.get("StreamModeDetails")
.and_then(|v| v.get("StreamMode"))
.and_then(|v| v.as_str())
.unwrap_or("PROVISIONED")
.to_string();
let retention_period_hours = props
.get("RetentionPeriodHours")
.and_then(|v| v.as_i64())
.unwrap_or(24) as i32;
let mut accounts = self.kinesis_state.write();
let state = accounts.get_or_create(&self.account_id);
if state.streams.contains_key(&stream_name) {
return Err(format!("Stream {stream_name} already exists"));
}
let stream_arn = format!(
"arn:aws:kinesis:{}:{}:stream/{}",
state.region, state.account_id, stream_name
);
let stream = KinesisStream {
stream_name: stream_name.clone(),
stream_arn: stream_arn.clone(),
stream_status: "ACTIVE".to_string(),
stream_creation_timestamp: Utc::now(),
retention_period_hours,
stream_mode,
encryption_type: "NONE".to_string(),
key_id: None,
shard_count,
open_shard_count: shard_count,
tags: BTreeMap::new(),
shards: build_stream_shards(shard_count),
next_shard_index: shard_count,
enhanced_metrics: Vec::new(),
warm_throughput_mibps: None,
max_record_size_kib: None,
};
state.streams.insert(stream_name.clone(), stream);
Ok(ProvisionResult::new(stream_name).with("Arn", stream_arn))
}
fn delete_kinesis_stream(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.kinesis_state.write();
let state = accounts.get_or_create(&self.account_id);
state.streams.remove(physical_id);
Ok(())
}
fn create_kinesis_stream_consumer(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let stream_arn = props
.get("StreamARN")
.and_then(|v| v.as_str())
.ok_or_else(|| "StreamARN is required".to_string())?
.to_string();
let consumer_name = props
.get("ConsumerName")
.and_then(|v| v.as_str())
.ok_or_else(|| "ConsumerName is required".to_string())?
.to_string();
let mut accounts = self.kinesis_state.write();
let state = accounts.get_or_create(&self.account_id);
if state
.consumers
.values()
.any(|c| c.stream_arn == stream_arn && c.consumer_name == consumer_name)
{
return Err(format!(
"Consumer {consumer_name} already exists on stream {stream_arn}"
));
}
let now = Utc::now();
let consumer_arn = format!(
"{}/consumer/{}:{}",
stream_arn,
consumer_name,
now.timestamp()
);
let consumer = KinesisConsumer {
consumer_name: consumer_name.clone(),
consumer_arn: consumer_arn.clone(),
consumer_status: "ACTIVE".to_string(),
consumer_creation_timestamp: now,
stream_arn: stream_arn.clone(),
};
state.consumers.insert(consumer_arn.clone(), consumer);
Ok(ProvisionResult::new(consumer_arn.clone())
.with("ConsumerARN", consumer_arn)
.with("ConsumerName", consumer_name)
.with("ConsumerStatus", "ACTIVE")
.with("ConsumerCreationTimestamp", now.timestamp().to_string())
.with("StreamARN", stream_arn))
}
fn delete_kinesis_stream_consumer(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.kinesis_state.write();
let state = accounts.get_or_create(&self.account_id);
state.consumers.remove(physical_id);
Ok(())
}
fn create_kms_key(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let input = parse_kms_key_input(&resource.properties);
let (key_id, arn) =
kms_provisioner::provision_key(&self.kms_state, &self.account_id, &input)?;
Ok(ProvisionResult::new(key_id.clone())
.with("Arn", arn)
.with("KeyId", key_id))
}
fn update_kms_key(
&self,
existing: &StackResource,
new_def: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let new_input = parse_kms_key_input(&new_def.properties);
let arn = {
let mut accounts = self.kms_state.write();
let state = accounts.get_or_create(&self.account_id);
let key = state
.keys
.get(&existing.physical_id)
.ok_or_else(|| format!("Key '{}' does not exist", existing.physical_id))?;
if key.key_spec != new_input.key_spec
|| key.key_usage != new_input.key_usage
|| key.origin != new_input.origin
|| key.multi_region != new_input.multi_region
{
return Err(
"AWS::KMS::Key updates that change KeySpec, KeyUsage, Origin, or MultiRegion require replacement"
.to_string(),
);
}
key.arn.clone()
};
kms_provisioner::update_key_properties(
&self.kms_state,
&self.account_id,
&existing.physical_id,
kms_provisioner::KeyUpdate {
description: Some(new_input.description.clone()),
enabled: Some(new_input.enabled),
key_rotation_enabled: Some(new_input.key_rotation_enabled),
policy: new_input.policy.clone(),
tags: Some(new_input.tags.clone()),
},
)?;
Ok(ProvisionResult::new(existing.physical_id.clone())
.with("Arn", arn)
.with("KeyId", existing.physical_id.clone()))
}
fn delete_kms_key(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.kms_state.write();
let state = accounts.get_or_create(&self.account_id);
state.keys.remove(physical_id);
state.aliases.retain(|_, a| a.target_key_id != physical_id);
Ok(())
}
fn create_kms_replica_key(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let primary_arn = props
.get("PrimaryKeyArn")
.and_then(|v| v.as_str())
.ok_or_else(|| "PrimaryKeyArn is required".to_string())?
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(str::to_string);
let enabled = props
.get("Enabled")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let policy = parse_key_policy(props);
let tags = parse_tag_list(props);
let (replica_key_id, replica_arn) = kms_provisioner::provision_replica_key(
&self.kms_state,
&self.account_id,
&primary_arn,
description,
enabled,
policy,
tags,
)?;
Ok(ProvisionResult::new(replica_key_id.clone())
.with("KeyId", replica_key_id)
.with("Arn", replica_arn))
}
fn update_kms_replica_key(
&self,
existing: &StackResource,
new_def: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &new_def.properties;
let new_primary = props
.get("PrimaryKeyArn")
.and_then(|v| v.as_str())
.ok_or_else(|| "PrimaryKeyArn is required".to_string())?;
{
let mut accounts = self.kms_state.write();
let state = accounts.get_or_create(&self.account_id);
let key = state
.keys
.get(&existing.physical_id)
.ok_or_else(|| format!("ReplicaKey '{}' does not exist", existing.physical_id))?;
if let Some(existing_region) = key.primary_region.as_deref() {
let parts: Vec<&str> = new_primary.split(':').collect();
if parts.len() < 4 || parts[3] != existing_region {
return Err(
"AWS::KMS::ReplicaKey updates that change PrimaryKeyArn require replacement"
.to_string(),
);
}
}
}
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(str::to_string);
let enabled = props
.get("Enabled")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let policy = parse_key_policy(props);
let tags = parse_tag_list(props);
kms_provisioner::update_key_properties(
&self.kms_state,
&self.account_id,
&existing.physical_id,
kms_provisioner::KeyUpdate {
description,
enabled: Some(enabled),
key_rotation_enabled: None,
policy,
tags: Some(tags),
},
)?;
let arn = {
let mut accounts = self.kms_state.write();
let state = accounts.get_or_create(&self.account_id);
state
.keys
.get(&existing.physical_id)
.map(|k| k.arn.clone())
.unwrap_or_default()
};
Ok(ProvisionResult::new(existing.physical_id.clone())
.with("KeyId", existing.physical_id.clone())
.with("Arn", arn))
}
fn delete_kms_replica_key(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.kms_state.write();
let state = accounts.get_or_create(&self.account_id);
state.keys.remove(physical_id);
Ok(())
}
fn create_kms_alias(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let alias_name = props
.get("AliasName")
.and_then(|v| v.as_str())
.ok_or_else(|| "AliasName is required".to_string())?
.to_string();
let target_input = props
.get("TargetKeyId")
.and_then(|v| v.as_str())
.ok_or_else(|| "TargetKeyId is required".to_string())?
.to_string();
let alias = kms_provisioner::provision_alias(
&self.kms_state,
&self.account_id,
&alias_name,
&target_input,
)?;
Ok(ProvisionResult::new(alias))
}
fn update_kms_alias(
&self,
existing: &StackResource,
new_def: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &new_def.properties;
let new_alias_name = props
.get("AliasName")
.and_then(|v| v.as_str())
.ok_or_else(|| "AliasName is required".to_string())?;
if new_alias_name != existing.physical_id {
return Err(
"AWS::KMS::Alias updates that change AliasName require replacement".to_string(),
);
}
let target_input = props
.get("TargetKeyId")
.and_then(|v| v.as_str())
.ok_or_else(|| "TargetKeyId is required".to_string())?;
kms_provisioner::update_alias_target(
&self.kms_state,
&self.account_id,
&existing.physical_id,
target_input,
)?;
Ok(ProvisionResult::new(existing.physical_id.clone()))
}
fn delete_kms_alias(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.kms_state.write();
let state = accounts.get_or_create(&self.account_id);
state.aliases.remove(physical_id);
Ok(())
}
fn create_ecr_repository(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let repository_name = props
.get("RepositoryName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let image_tag_mutability = props
.get("ImageTagMutability")
.and_then(|v| v.as_str())
.unwrap_or("MUTABLE")
.to_string();
let scan_on_push = props
.get("ImageScanningConfiguration")
.and_then(|v| v.get("ScanOnPush"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let encryption_type = props
.get("EncryptionConfiguration")
.and_then(|v| v.get("EncryptionType"))
.and_then(|v| v.as_str())
.unwrap_or("AES256")
.to_string();
let kms_key = props
.get("EncryptionConfiguration")
.and_then(|v| v.get("KmsKey"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let policy_text = props
.get("RepositoryPolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.filter(|s| !s.is_empty());
let lifecycle_policy = props
.get("LifecyclePolicy")
.and_then(|v| v.get("LifecyclePolicyText"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut tags: BTreeMap<String, String> = BTreeMap::new();
if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
for t in arr {
if let (Some(k), Some(v)) = (
t.get("Key").and_then(|x| x.as_str()),
t.get("Value").and_then(|x| x.as_str()),
) {
tags.insert(k.to_string(), v.to_string());
}
}
}
let empty_on_delete = props
.get("EmptyOnDelete")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
if state.repositories.contains_key(&repository_name) {
return Err(format!("Repository {repository_name} already exists"));
}
let arn = state.repository_arn(&repository_name);
let registry_id = state.account_id.clone();
let endpoint = format!(
"{}.dkr.ecr.{}.amazonaws.com",
state.account_id, state.region
);
let mut repo = Repository::new(&repository_name, arn.clone(), ®istry_id, &endpoint);
repo.image_tag_mutability = image_tag_mutability;
repo.image_scanning_configuration.scan_on_push = scan_on_push;
repo.encryption_configuration.encryption_type = encryption_type;
repo.encryption_configuration.kms_key = kms_key;
repo.policy = policy_text;
if let Some(policy) = lifecycle_policy.as_ref() {
let prune = fakecloud_ecr::evaluate_lifecycle_policy(&repo, policy);
for digest in &prune {
repo.images.remove(digest);
repo.image_tags.retain(|_, d| d != digest);
}
repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
}
repo.lifecycle_policy = lifecycle_policy;
repo.tags = tags;
let uri = repo.repository_uri.clone();
state.repositories.insert(repository_name.clone(), repo);
Ok(ProvisionResult::new(repository_name)
.with("Arn", arn)
.with("RepositoryUri", uri)
.with("RegistryId", registry_id)
.with("EmptyOnDelete", empty_on_delete.to_string()))
}
fn delete_ecr_repository(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.repositories.remove(physical_id);
Ok(())
}
fn create_ecr_repository_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let repository_name = props
.get("RepositoryName")
.and_then(|v| v.as_str())
.ok_or_else(|| "RepositoryName is required".to_string())?
.to_string();
let policy_text = props
.get("PolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "PolicyText is required".to_string())?;
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let repo = state
.repositories
.get_mut(&repository_name)
.ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
repo.policy = Some(policy_text);
Ok(ProvisionResult::new(format!(
"{}/{}",
self.account_id, repository_name
)))
}
fn delete_ecr_repository_policy(&self, physical_id: &str) -> Result<(), String> {
let repository_name = physical_id
.split_once('/')
.map(|(_, n)| n.to_string())
.unwrap_or_else(|| physical_id.to_string());
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(repo) = state.repositories.get_mut(&repository_name) {
repo.policy = None;
}
Ok(())
}
fn create_ecr_registry_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let policy_text = props
.get("PolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "PolicyText is required".to_string())?;
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.registry_policy = Some(policy_text);
Ok(ProvisionResult::new(self.account_id.clone())
.with("RegistryId", self.account_id.clone()))
}
fn delete_ecr_registry_policy(&self) -> Result<(), String> {
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.registry_policy = None;
Ok(())
}
fn create_ecr_replication_configuration(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
use fakecloud_ecr::state::{
ReplicationConfiguration, ReplicationDestination, ReplicationRule, RepositoryFilter,
};
let cfg = resource
.properties
.get("ReplicationConfiguration")
.ok_or_else(|| "ReplicationConfiguration is required".to_string())?;
let rules: Vec<ReplicationRule> = cfg
.get("Rules")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|r| {
let destinations: Vec<ReplicationDestination> = r
.get("Destinations")
.and_then(|v| v.as_array())
.map(|d| {
d.iter()
.map(|dest| ReplicationDestination {
region: dest
.get("Region")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
registry_id: dest
.get("RegistryId")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
})
.collect()
})
.unwrap_or_default();
let repository_filters: Vec<RepositoryFilter> = r
.get("RepositoryFilters")
.and_then(|v| v.as_array())
.map(|f| {
f.iter()
.map(|f| RepositoryFilter {
filter: f
.get("Filter")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
filter_type: f
.get("FilterType")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
})
.collect()
})
.unwrap_or_default();
ReplicationRule {
destinations,
repository_filters,
}
})
.collect()
})
.unwrap_or_default();
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.replication_configuration = Some(ReplicationConfiguration { rules });
Ok(ProvisionResult::new(self.account_id.clone()))
}
fn delete_ecr_replication_configuration(&self) -> Result<(), String> {
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.replication_configuration = None;
Ok(())
}
fn create_ecr_pull_through_cache_rule(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
use fakecloud_ecr::state::PullThroughCacheRule;
let props = &resource.properties;
let prefix = props
.get("EcrRepositoryPrefix")
.and_then(|v| v.as_str())
.ok_or_else(|| "EcrRepositoryPrefix is required".to_string())?
.to_string();
let upstream_url = props
.get("UpstreamRegistryUrl")
.and_then(|v| v.as_str())
.ok_or_else(|| "UpstreamRegistryUrl is required".to_string())?
.to_string();
let upstream_registry = props
.get("UpstreamRegistry")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let credential_arn = props
.get("CredentialArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let custom_role_arn = props
.get("CustomRoleArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let now = Utc::now();
let rule = PullThroughCacheRule {
ecr_repository_prefix: prefix.clone(),
upstream_registry_url: upstream_url,
upstream_registry,
credential_arn,
created_at: now,
updated_at: now,
custom_role_arn,
};
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.pull_through_cache_rules.insert(prefix.clone(), rule);
Ok(ProvisionResult::new(prefix))
}
fn delete_ecr_pull_through_cache_rule(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.pull_through_cache_rules.remove(physical_id);
Ok(())
}
fn create_ecr_lifecycle_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let repository_name = props
.get("RepositoryName")
.and_then(|v| v.as_str())
.ok_or_else(|| "RepositoryName is required".to_string())?
.to_string();
let policy_text = props
.get("LifecyclePolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "LifecyclePolicyText is required".to_string())?;
let _registry_id = props
.get("RegistryId")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let repo = state
.repositories
.get_mut(&repository_name)
.ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, &policy_text);
for digest in &prune {
repo.images.remove(digest);
repo.image_tags.retain(|_, d| d != digest);
}
repo.lifecycle_policy = Some(policy_text);
repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
let registry_id = repo.registry_id.clone();
Ok(
ProvisionResult::new(format!("{}/{}", self.account_id, repository_name))
.with("RepositoryName", repository_name)
.with("RegistryId", registry_id),
)
}
fn delete_ecr_lifecycle_policy(&self, physical_id: &str) -> Result<(), String> {
let repository_name = physical_id
.split_once('/')
.map(|(_, n)| n.to_string())
.unwrap_or_else(|| physical_id.to_string());
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(repo) = state.repositories.get_mut(&repository_name) {
repo.lifecycle_policy = None;
repo.lifecycle_policy_last_evaluated_at = None;
}
Ok(())
}
fn create_ecr_registry_scanning_configuration(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
use fakecloud_ecr::state::{
RegistryScanningConfiguration, RegistryScanningRule, RepositoryFilter,
};
let props = &resource.properties;
let scan_type = props
.get("ScanType")
.and_then(|v| v.as_str())
.unwrap_or("BASIC")
.to_string();
let rules: Vec<RegistryScanningRule> = props
.get("Rules")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|r| {
let scan_frequency = r
.get("ScanFrequency")
.and_then(|v| v.as_str())
.unwrap_or("MANUAL")
.to_string();
let repository_filters: Vec<RepositoryFilter> = r
.get("RepositoryFilters")
.and_then(|v| v.as_array())
.map(|f| {
f.iter()
.map(|f| RepositoryFilter {
filter: f
.get("Filter")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
filter_type: f
.get("FilterType")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
})
.collect()
})
.unwrap_or_default();
RegistryScanningRule {
scan_frequency,
repository_filters,
}
})
.collect()
})
.unwrap_or_default();
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.registry_scanning_configuration = RegistryScanningConfiguration { scan_type, rules };
Ok(ProvisionResult::new(self.account_id.clone()))
}
fn delete_ecr_registry_scanning_configuration(&self) -> Result<(), String> {
use fakecloud_ecr::state::RegistryScanningConfiguration;
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.registry_scanning_configuration = RegistryScanningConfiguration::default();
Ok(())
}
fn update_ecr_repository(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let repository_name = existing.physical_id.clone();
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let repo = state
.repositories
.get_mut(&repository_name)
.ok_or_else(|| format!("Repository {repository_name} no longer exists"))?;
if let Some(s) = props.get("ImageTagMutability").and_then(|v| v.as_str()) {
repo.image_tag_mutability = s.to_string();
}
if let Some(b) = props
.get("ImageScanningConfiguration")
.and_then(|v| v.get("ScanOnPush"))
.and_then(|v| v.as_bool())
{
repo.image_scanning_configuration.scan_on_push = b;
}
if let Some(cfg) = props.get("EncryptionConfiguration") {
if let Some(s) = cfg.get("EncryptionType").and_then(|v| v.as_str()) {
repo.encryption_configuration.encryption_type = s.to_string();
}
if let Some(s) = cfg.get("KmsKey").and_then(|v| v.as_str()) {
repo.encryption_configuration.kms_key = Some(s.to_string());
}
}
if let Some(v) = props.get("RepositoryPolicyText") {
let text = if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
};
repo.policy = if text.is_empty() { None } else { Some(text) };
}
if let Some(text) = props
.get("LifecyclePolicy")
.and_then(|v| v.get("LifecyclePolicyText"))
.and_then(|v| v.as_str())
{
let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, text);
for digest in &prune {
repo.images.remove(digest);
repo.image_tags.retain(|_, d| d != digest);
}
repo.lifecycle_policy = Some(text.to_string());
repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
}
if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
let mut tags: BTreeMap<String, String> = BTreeMap::new();
for t in arr {
if let (Some(k), Some(v)) = (
t.get("Key").and_then(|x| x.as_str()),
t.get("Value").and_then(|x| x.as_str()),
) {
tags.insert(k.to_string(), v.to_string());
}
}
repo.tags = tags;
}
let arn = repo.repository_arn.clone();
let uri = repo.repository_uri.clone();
let registry_id = repo.registry_id.clone();
Ok(ProvisionResult::new(repository_name)
.with("Arn", arn)
.with("RepositoryUri", uri)
.with("RegistryId", registry_id))
}
fn update_ecr_repository_policy(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let physical_id = existing.physical_id.clone();
let repository_name = physical_id
.split_once('/')
.map(|(_, n)| n.to_string())
.unwrap_or_else(|| physical_id.clone());
let policy_text = props
.get("PolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "PolicyText is required".to_string())?;
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let repo = state
.repositories
.get_mut(&repository_name)
.ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
repo.policy = Some(policy_text);
Ok(ProvisionResult::new(physical_id))
}
fn update_ecr_lifecycle_policy(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let physical_id = existing.physical_id.clone();
let repository_name = physical_id
.split_once('/')
.map(|(_, n)| n.to_string())
.unwrap_or_else(|| physical_id.clone());
let policy_text = props
.get("LifecyclePolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "LifecyclePolicyText is required".to_string())?;
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let repo = state
.repositories
.get_mut(&repository_name)
.ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, &policy_text);
for digest in &prune {
repo.images.remove(digest);
repo.image_tags.retain(|_, d| d != digest);
}
repo.lifecycle_policy = Some(policy_text);
repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
let registry_id = repo.registry_id.clone();
Ok(ProvisionResult::new(physical_id)
.with("RepositoryName", repository_name)
.with("RegistryId", registry_id))
}
fn update_ecr_registry_policy(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let policy_text = props
.get("PolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "PolicyText is required".to_string())?;
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.registry_policy = Some(policy_text);
Ok(ProvisionResult::new(existing.physical_id.clone())
.with("RegistryId", self.account_id.clone()))
}
fn update_ecr_replication_configuration(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let result = self.create_ecr_replication_configuration(resource)?;
Ok(ProvisionResult::new(existing.physical_id.clone()).merge_attributes(result.attributes))
}
fn update_ecr_registry_scanning_configuration(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let result = self.create_ecr_registry_scanning_configuration(resource)?;
Ok(ProvisionResult::new(existing.physical_id.clone()).merge_attributes(result.attributes))
}
fn update_ecr_pull_through_cache_rule(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let prefix = existing.physical_id.clone();
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let rule = state
.pull_through_cache_rules
.get_mut(&prefix)
.ok_or_else(|| format!("PullThroughCacheRule {prefix} no longer exists"))?;
if let Some(s) = props.get("UpstreamRegistryUrl").and_then(|v| v.as_str()) {
rule.upstream_registry_url = s.to_string();
}
if let Some(s) = props.get("UpstreamRegistry").and_then(|v| v.as_str()) {
rule.upstream_registry = Some(s.to_string());
}
if let Some(s) = props.get("CredentialArn").and_then(|v| v.as_str()) {
rule.credential_arn = Some(s.to_string());
}
if let Some(s) = props.get("CustomRoleArn").and_then(|v| v.as_str()) {
rule.custom_role_arn = Some(s.to_string());
}
rule.updated_at = Utc::now();
Ok(ProvisionResult::new(prefix))
}
fn get_att_ecr_repository(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let repo = state.repositories.get(physical_id)?;
match attribute {
"Arn" => Some(repo.repository_arn.clone()),
"RepositoryUri" => Some(repo.repository_uri.clone()),
"RegistryId" => Some(repo.registry_id.clone()),
_ => None,
}
}
fn create_cloudwatch_alarm(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let alarm_name = props
.get("AlarmName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let alarm_description = props
.get("AlarmDescription")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let actions_enabled = props
.get("ActionsEnabled")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let str_array = |key: &str| -> Vec<String> {
props
.get(key)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|x| x.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default()
};
let alarm_actions = str_array("AlarmActions");
let ok_actions = str_array("OKActions");
let insufficient_data_actions = str_array("InsufficientDataActions");
let metric_name = props
.get("MetricName")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let namespace = props
.get("Namespace")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let statistic = props
.get("Statistic")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let extended_statistic = props
.get("ExtendedStatistic")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let unit = props
.get("Unit")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let period = props.get("Period").and_then(|v| v.as_i64());
let evaluation_periods = props
.get("EvaluationPeriods")
.and_then(|v| v.as_i64())
.unwrap_or(1);
let datapoints_to_alarm = props.get("DatapointsToAlarm").and_then(|v| v.as_i64());
let threshold = props.get("Threshold").and_then(|v| v.as_f64());
let comparison_operator = props
.get("ComparisonOperator")
.and_then(|v| v.as_str())
.unwrap_or("GreaterThanThreshold")
.to_string();
let treat_missing_data = props
.get("TreatMissingData")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let evaluate_low_sample_count_percentile = props
.get("EvaluateLowSampleCountPercentile")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut dimensions: BTreeMap<String, String> = BTreeMap::new();
if let Some(arr) = props.get("Dimensions").and_then(|v| v.as_array()) {
for d in arr {
if let (Some(k), Some(v)) = (
d.get("Name").and_then(|x| x.as_str()),
d.get("Value").and_then(|x| x.as_str()),
) {
dimensions.insert(k.to_string(), v.to_string());
}
}
}
let mut accounts = self.cloudwatch_state.write();
let state = accounts.get_or_create(&self.account_id);
let alarm_arn = format!(
"arn:aws:cloudwatch:{}:{}:alarm:{}",
self.region, self.account_id, alarm_name
);
let now = Utc::now();
let alarm = MetricAlarm {
alarm_name: alarm_name.clone(),
alarm_arn: alarm_arn.clone(),
alarm_description,
actions_enabled,
ok_actions,
alarm_actions,
insufficient_data_actions,
state_value: AlarmState::InsufficientData,
state_reason: "Unchecked: Initial alarm creation".to_string(),
state_updated_timestamp: now,
metric_name,
namespace,
statistic,
extended_statistic,
dimensions,
period,
unit,
evaluation_periods,
datapoints_to_alarm,
threshold,
comparison_operator,
treat_missing_data,
evaluate_low_sample_count_percentile,
configuration_updated_timestamp: now,
alarm_configuration_updated_timestamp: now,
};
let region_alarms = state.alarms_in_mut(&self.region);
if region_alarms.contains_key(&alarm_name) {
return Err(format!("Alarm {alarm_name} already exists"));
}
region_alarms.insert(alarm_name.clone(), alarm);
Ok(ProvisionResult::new(alarm_name).with("Arn", alarm_arn))
}
fn delete_cloudwatch_alarm(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cloudwatch_state.write();
let state = accounts.get_or_create(&self.account_id);
state.alarms_in_mut(&self.region).remove(physical_id);
Ok(())
}
fn create_cloudwatch_dashboard(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let dashboard_name = props
.get("DashboardName")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| {
let suffix = Uuid::new_v4().simple().to_string();
format!("{}-{}", resource.logical_id, &suffix[..8])
});
let body = props
.get("DashboardBody")
.ok_or("DashboardBody is required")?;
let body_str = if let Some(s) = body.as_str() {
s.to_string()
} else {
serde_json::to_string(body).map_err(|e| format!("invalid DashboardBody: {e}"))?
};
serde_json::from_str::<serde_json::Value>(&body_str)
.map_err(|e| format!("DashboardBody must be valid JSON: {e}"))?;
let arn = format!(
"arn:aws:cloudwatch::{}:dashboard/{dashboard_name}",
self.account_id
);
let dashboard = Dashboard {
name: dashboard_name.clone(),
arn: arn.clone(),
size_bytes: body_str.len() as i64,
body: body_str,
last_modified: Utc::now(),
};
let mut accounts = self.cloudwatch_state.write();
let state = accounts.get_or_create(&self.account_id);
state.dashboards.insert(dashboard_name.clone(), dashboard);
Ok(ProvisionResult::new(dashboard_name).with("Arn", arn))
}
fn delete_cloudwatch_dashboard(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cloudwatch_state.write();
let state = accounts.get_or_create(&self.account_id);
state.dashboards.remove(physical_id);
Ok(())
}
fn update_cloudwatch_alarm(
&self,
existing: &StackResource,
new_def: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &new_def.properties;
let new_alarm_name = props
.get("AlarmName")
.and_then(|v| v.as_str())
.unwrap_or(&new_def.logical_id);
if new_alarm_name != existing.physical_id {
return Err(
"AWS::CloudWatch::Alarm updates that change AlarmName require replacement"
.to_string(),
);
}
let str_array = |key: &str| -> Vec<String> {
props
.get(key)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|x| x.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default()
};
let alarm_description = props
.get("AlarmDescription")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let actions_enabled = props
.get("ActionsEnabled")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let alarm_actions = str_array("AlarmActions");
let ok_actions = str_array("OKActions");
let insufficient_data_actions = str_array("InsufficientDataActions");
let metric_name = props
.get("MetricName")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let namespace = props
.get("Namespace")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let statistic = props
.get("Statistic")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let extended_statistic = props
.get("ExtendedStatistic")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let unit = props
.get("Unit")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let period = props.get("Period").and_then(|v| v.as_i64());
let evaluation_periods = props
.get("EvaluationPeriods")
.and_then(|v| v.as_i64())
.unwrap_or(1);
let datapoints_to_alarm = props.get("DatapointsToAlarm").and_then(|v| v.as_i64());
let threshold = props.get("Threshold").and_then(|v| v.as_f64());
let comparison_operator = props
.get("ComparisonOperator")
.and_then(|v| v.as_str())
.unwrap_or("GreaterThanThreshold")
.to_string();
let treat_missing_data = props
.get("TreatMissingData")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let evaluate_low_sample_count_percentile = props
.get("EvaluateLowSampleCountPercentile")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut dimensions: BTreeMap<String, String> = BTreeMap::new();
if let Some(arr) = props.get("Dimensions").and_then(|v| v.as_array()) {
for d in arr {
if let (Some(k), Some(v)) = (
d.get("Name").and_then(|x| x.as_str()),
d.get("Value").and_then(|x| x.as_str()),
) {
dimensions.insert(k.to_string(), v.to_string());
}
}
}
let mut accounts = self.cloudwatch_state.write();
let state = accounts.get_or_create(&self.account_id);
let region_alarms = state.alarms_in_mut(&self.region);
let alarm = region_alarms
.get_mut(&existing.physical_id)
.ok_or_else(|| format!("Alarm {} not found", existing.physical_id))?;
let now = Utc::now();
alarm.alarm_description = alarm_description;
alarm.actions_enabled = actions_enabled;
alarm.ok_actions = ok_actions;
alarm.alarm_actions = alarm_actions;
alarm.insufficient_data_actions = insufficient_data_actions;
alarm.metric_name = metric_name;
alarm.namespace = namespace;
alarm.statistic = statistic;
alarm.extended_statistic = extended_statistic;
alarm.dimensions = dimensions;
alarm.period = period;
alarm.unit = unit;
alarm.evaluation_periods = evaluation_periods;
alarm.datapoints_to_alarm = datapoints_to_alarm;
alarm.threshold = threshold;
alarm.comparison_operator = comparison_operator;
alarm.treat_missing_data = treat_missing_data;
alarm.evaluate_low_sample_count_percentile = evaluate_low_sample_count_percentile;
alarm.configuration_updated_timestamp = now;
alarm.alarm_configuration_updated_timestamp = now;
let alarm_arn = alarm.alarm_arn.clone();
Ok(ProvisionResult::new(existing.physical_id.clone()).with("Arn", alarm_arn))
}
fn update_cloudwatch_dashboard(
&self,
existing: &StackResource,
new_def: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &new_def.properties;
if let Some(new_name) = props.get("DashboardName").and_then(|v| v.as_str()) {
if new_name != existing.physical_id {
return Err(
"AWS::CloudWatch::Dashboard updates that change DashboardName require replacement"
.to_string(),
);
}
}
let body = props
.get("DashboardBody")
.ok_or("DashboardBody is required")?;
let body_str = if let Some(s) = body.as_str() {
s.to_string()
} else {
serde_json::to_string(body).map_err(|e| format!("invalid DashboardBody: {e}"))?
};
serde_json::from_str::<serde_json::Value>(&body_str)
.map_err(|e| format!("DashboardBody must be valid JSON: {e}"))?;
let mut accounts = self.cloudwatch_state.write();
let state = accounts.get_or_create(&self.account_id);
let dashboard = state
.dashboards
.get_mut(&existing.physical_id)
.ok_or_else(|| format!("Dashboard {} not found", existing.physical_id))?;
dashboard.size_bytes = body_str.len() as i64;
dashboard.body = body_str;
dashboard.last_modified = Utc::now();
let arn = dashboard.arn.clone();
Ok(ProvisionResult::new(existing.physical_id.clone()).with("Arn", arn))
}
fn create_elbv2_load_balancer(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let scheme = props
.get("Scheme")
.and_then(|v| v.as_str())
.unwrap_or("internet-facing")
.to_string();
let lb_type = props
.get("Type")
.and_then(|v| v.as_str())
.unwrap_or("application")
.to_string();
let ip_address_type = props
.get("IpAddressType")
.and_then(|v| v.as_str())
.unwrap_or("ipv4")
.to_string();
let security_groups: Vec<String> = props
.get("SecurityGroups")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|s| s.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let tags = parse_elb_tags(props.get("Tags"));
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let lb_id = Uuid::new_v4().simple().to_string();
let arn = format!(
"arn:aws:elasticloadbalancing:{}:{}:loadbalancer/{}/{}/{}",
self.region,
self.account_id,
if lb_type == "network" { "net" } else { "app" },
name,
&lb_id[..16]
);
let dns_name = format!(
"{}-{}.{}.elb.{}.amazonaws.com",
name,
&lb_id[..16],
self.region,
self.region
);
let mut availability_zones: Vec<fakecloud_elbv2::AvailabilityZone> = Vec::new();
if let Some(arr) = props.get("Subnets").and_then(|v| v.as_array()) {
for s in arr {
if let Some(subnet_id) = s.as_str() {
availability_zones.push(fakecloud_elbv2::AvailabilityZone {
zone_name: format!("{}a", self.region),
subnet_id: subnet_id.to_string(),
outpost_id: None,
load_balancer_addresses: Vec::new(),
source_nat_ipv6_prefixes: Vec::new(),
});
}
}
}
state.load_balancers.insert(
arn.clone(),
LoadBalancer {
arn: arn.clone(),
name: name.clone(),
dns_name: dns_name.clone(),
canonical_hosted_zone_id: "Z2P70J7EXAMPLE".to_string(),
created_time: Utc::now(),
scheme,
vpc_id: String::new(),
state_code: "active".to_string(),
state_reason: None,
lb_type,
availability_zones,
security_groups,
ip_address_type,
customer_owned_ipv4_pool: None,
enforce_security_group_inbound_rules_on_private_link_traffic: None,
enable_prefix_for_ipv6_source_nat: None,
ipv4_ipam_pool_id: None,
tags,
attributes: BTreeMap::new(),
minimum_capacity_units: None,
bound_port: None,
},
);
Ok(ProvisionResult::new(arn.clone())
.with("LoadBalancerArn", arn)
.with(
"LoadBalancerFullName",
format!("app/{name}/{}", &lb_id[..16]),
)
.with("LoadBalancerName", name)
.with("DNSName", dns_name)
.with("CanonicalHostedZoneID", "Z2P70J7EXAMPLE"))
}
fn delete_elbv2_load_balancer(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
state.load_balancers.remove(physical_id);
let listeners: Vec<String> = state
.listeners
.iter()
.filter(|(_, l)| l.load_balancer_arn == physical_id)
.map(|(arn, _)| arn.clone())
.collect();
for arn in &listeners {
state.listeners.remove(arn);
let rules: Vec<String> = state
.rules
.iter()
.filter(|(_, r)| r.listener_arn == *arn)
.map(|(a, _)| a.clone())
.collect();
for r in rules {
state.rules.remove(&r);
}
}
for tg in state.target_groups.values_mut() {
tg.load_balancer_arns.retain(|a| a != physical_id);
}
Ok(())
}
fn create_elbv2_target_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let protocol = props
.get("Protocol")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let port = props.get("Port").and_then(|v| v.as_i64()).map(|n| n as i32);
let vpc_id = props
.get("VpcId")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let target_type = props
.get("TargetType")
.and_then(|v| v.as_str())
.unwrap_or("instance")
.to_string();
let ip_address_type = props
.get("IpAddressType")
.and_then(|v| v.as_str())
.unwrap_or("ipv4")
.to_string();
let protocol_version = props
.get("ProtocolVersion")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let tags = parse_elb_tags(props.get("Tags"));
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let id = Uuid::new_v4().simple().to_string();
let arn = format!(
"arn:aws:elasticloadbalancing:{}:{}:targetgroup/{}/{}",
self.region,
self.account_id,
name,
&id[..16]
);
state.target_groups.insert(
arn.clone(),
TargetGroup {
arn: arn.clone(),
name: name.clone(),
protocol,
port,
vpc_id,
target_type,
ip_address_type,
protocol_version,
health_check_protocol: props
.get("HealthCheckProtocol")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
health_check_port: props
.get("HealthCheckPort")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
health_check_enabled: props
.get("HealthCheckEnabled")
.and_then(|v| v.as_bool())
.unwrap_or(true),
health_check_path: props
.get("HealthCheckPath")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
health_check_interval_seconds: props
.get("HealthCheckIntervalSeconds")
.and_then(|v| v.as_i64())
.unwrap_or(30) as i32,
health_check_timeout_seconds: props
.get("HealthCheckTimeoutSeconds")
.and_then(|v| v.as_i64())
.unwrap_or(5) as i32,
healthy_threshold_count: props
.get("HealthyThresholdCount")
.and_then(|v| v.as_i64())
.unwrap_or(5) as i32,
unhealthy_threshold_count: props
.get("UnhealthyThresholdCount")
.and_then(|v| v.as_i64())
.unwrap_or(2) as i32,
matcher_http_code: props
.get("Matcher")
.and_then(|v| v.get("HttpCode"))
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
matcher_grpc_code: props
.get("Matcher")
.and_then(|v| v.get("GrpcCode"))
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
load_balancer_arns: Vec::new(),
targets: Vec::new(),
tags,
attributes: BTreeMap::new(),
created_time: Utc::now(),
},
);
Ok(ProvisionResult::new(arn.clone())
.with("TargetGroupArn", arn)
.with("TargetGroupName", name)
.with("TargetGroupFullName", format!("targetgroup/{}", &id[..16])))
}
fn delete_elbv2_target_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
state.target_groups.remove(physical_id);
Ok(())
}
fn create_elbv2_listener(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let load_balancer_arn = props
.get("LoadBalancerArn")
.and_then(|v| v.as_str())
.ok_or_else(|| "LoadBalancerArn is required".to_string())?
.to_string();
let port = props.get("Port").and_then(|v| v.as_i64()).map(|n| n as i32);
let protocol = props
.get("Protocol")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let default_actions = parse_elb_actions(props.get("DefaultActions"));
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.load_balancers.contains_key(&load_balancer_arn) {
return Err(format!(
"LoadBalancer {load_balancer_arn} not yet provisioned"
));
}
let lb_full = load_balancer_arn
.rsplit("loadbalancer/")
.next()
.unwrap_or("")
.to_string();
let listener_id = Uuid::new_v4().simple().to_string();
let arn = format!(
"arn:aws:elasticloadbalancing:{}:{}:listener/{}/{}",
self.region,
self.account_id,
lb_full,
&listener_id[..16]
);
for action in &default_actions {
if let Some(tg_arn) = &action.target_group_arn {
if let Some(tg) = state.target_groups.get_mut(tg_arn) {
if !tg.load_balancer_arns.contains(&load_balancer_arn) {
tg.load_balancer_arns.push(load_balancer_arn.clone());
}
}
}
if let Some(forward) = &action.forward {
for tgt in &forward.target_groups {
if let Some(tg) = state.target_groups.get_mut(&tgt.target_group_arn) {
if !tg.load_balancer_arns.contains(&load_balancer_arn) {
tg.load_balancer_arns.push(load_balancer_arn.clone());
}
}
}
}
}
state.listeners.insert(
arn.clone(),
Listener {
arn: arn.clone(),
load_balancer_arn,
port,
protocol,
certificates: Vec::new(),
ssl_policy: props
.get("SslPolicy")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
default_actions,
alpn_policy: Vec::new(),
mutual_authentication: None,
tags: parse_elb_tags(props.get("Tags")),
attributes: BTreeMap::new(),
},
);
Ok(ProvisionResult::new(arn.clone()).with("ListenerArn", arn))
}
fn delete_elbv2_listener(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
state.listeners.remove(physical_id);
let rules: Vec<String> = state
.rules
.iter()
.filter(|(_, r)| r.listener_arn == physical_id)
.map(|(arn, _)| arn.clone())
.collect();
for r in rules {
state.rules.remove(&r);
}
Ok(())
}
fn create_elbv2_listener_rule(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let listener_arn = props
.get("ListenerArn")
.and_then(|v| v.as_str())
.ok_or_else(|| "ListenerArn is required".to_string())?
.to_string();
let priority = props
.get("Priority")
.map(|v| {
if let Some(s) = v.as_str() {
s.to_string()
} else if let Some(n) = v.as_i64() {
n.to_string()
} else {
"1".to_string()
}
})
.unwrap_or_else(|| "1".to_string());
let actions = parse_elb_actions(props.get("Actions"));
let conditions = parse_elb_rule_conditions(props.get("Conditions"));
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.listeners.contains_key(&listener_arn) {
return Err(format!("Listener {listener_arn} not yet provisioned"));
}
let listener_full = listener_arn
.rsplit("listener/")
.next()
.unwrap_or("")
.to_string();
let rule_id = Uuid::new_v4().simple().to_string();
let arn = format!(
"arn:aws:elasticloadbalancing:{}:{}:listener-rule/{}/{}",
self.region,
self.account_id,
listener_full,
&rule_id[..16]
);
state.rules.insert(
arn.clone(),
ElbRule {
arn: arn.clone(),
listener_arn,
priority,
conditions,
actions,
is_default: false,
tags: parse_elb_tags(props.get("Tags")),
},
);
Ok(ProvisionResult::new(arn.clone()).with("RuleArn", arn))
}
fn delete_elbv2_listener_rule(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
state.rules.remove(physical_id);
Ok(())
}
fn create_elbv2_listener_certificate(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let listener_arn = props
.get("ListenerArn")
.and_then(|v| v.as_str())
.ok_or_else(|| "ListenerArn is required".to_string())?
.to_string();
let certs: Vec<String> = props
.get("Certificates")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|c| c.get("CertificateArn").and_then(|v| v.as_str()))
.map(|s| s.to_string())
.collect()
})
.unwrap_or_default();
if certs.is_empty() {
return Err("Certificates must contain at least one CertificateArn".to_string());
}
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let listener = state
.listeners
.get_mut(&listener_arn)
.ok_or_else(|| format!("Listener {listener_arn} does not exist"))?;
for arn in &certs {
listener.certificates.retain(|c| &c.certificate_arn != arn);
listener.certificates.push(fakecloud_elbv2::Certificate {
certificate_arn: arn.clone(),
is_default: false,
});
}
Ok(ProvisionResult::new(format!(
"{}#{}",
listener_arn,
certs.join(",")
)))
}
fn delete_elbv2_listener_certificate(&self, physical_id: &str) -> Result<(), String> {
let (listener_arn, cert_list) = match physical_id.split_once('#') {
Some(parts) => parts,
None => return Ok(()),
};
let cert_arns: Vec<&str> = cert_list.split(',').collect();
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(listener) = state.listeners.get_mut(listener_arn) {
listener
.certificates
.retain(|c| !cert_arns.iter().any(|a| *a == c.certificate_arn));
}
Ok(())
}
fn create_elbv2_trust_store(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let bucket = props
.get("CaCertificatesBundleS3Bucket")
.and_then(|v| v.as_str())
.ok_or_else(|| "CaCertificatesBundleS3Bucket is required".to_string())?;
let key = props
.get("CaCertificatesBundleS3Key")
.and_then(|v| v.as_str())
.ok_or_else(|| "CaCertificatesBundleS3Key is required".to_string())?;
let tags: Vec<fakecloud_elbv2::Tag> = props
.get("Tags")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|t| {
let k = t.get("Key").and_then(|v| v.as_str())?;
let val = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
Some(fakecloud_elbv2::Tag {
key: k.to_string(),
value: val.to_string(),
})
})
.collect()
})
.unwrap_or_default();
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if state.trust_stores.values().any(|t| t.name == name) {
return Err(format!("Trust store {name} already exists"));
}
let suffix: String = Uuid::new_v4()
.simple()
.to_string()
.chars()
.take(16)
.collect();
let arn = format!(
"arn:aws:elasticloadbalancing:{}:{}:truststore/{}/{}",
self.region, self.account_id, name, suffix
);
let ts = fakecloud_elbv2::TrustStore {
arn: arn.clone(),
name: name.clone(),
status: "ACTIVE".to_string(),
number_of_ca_certificates: 1,
total_revoked_entries: 0,
created_time: Utc::now(),
ca_certificates_bundle: Some(format!("s3://{bucket}/{key}").into_bytes()),
revocations: BTreeMap::new(),
next_revocation_id: 1,
tags,
};
state.trust_stores.insert(arn.clone(), ts);
Ok(ProvisionResult::new(arn.clone())
.with("TrustStoreArn", arn)
.with("Name", name)
.with("Status", "ACTIVE".to_string()))
}
fn delete_elbv2_trust_store(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
state.trust_stores.remove(physical_id);
Ok(())
}
fn update_elbv2_load_balancer(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let arn = existing.physical_id.clone();
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let lb = state
.load_balancers
.get_mut(&arn)
.ok_or_else(|| format!("LoadBalancer {arn} no longer exists"))?;
if let Some(arr) = props.get("SecurityGroups").and_then(|v| v.as_array()) {
lb.security_groups = arr
.iter()
.filter_map(|s| s.as_str().map(|s| s.to_string()))
.collect();
}
if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
lb.ip_address_type = s.to_string();
}
if let Some(arr) = props.get("Subnets").and_then(|v| v.as_array()) {
let mut zones: Vec<fakecloud_elbv2::AvailabilityZone> = Vec::new();
for s in arr {
if let Some(subnet_id) = s.as_str() {
zones.push(fakecloud_elbv2::AvailabilityZone {
zone_name: format!("{}a", self.region),
subnet_id: subnet_id.to_string(),
outpost_id: None,
load_balancer_addresses: Vec::new(),
source_nat_ipv6_prefixes: Vec::new(),
});
}
}
lb.availability_zones = zones;
}
if props.get("Tags").is_some() {
lb.tags = parse_elb_tags(props.get("Tags"));
}
let name = lb.name.clone();
let dns_name = lb.dns_name.clone();
let canonical = lb.canonical_hosted_zone_id.clone();
let lb_full = arn.rsplit("loadbalancer/").next().unwrap_or("").to_string();
Ok(ProvisionResult::new(arn.clone())
.with("LoadBalancerArn", arn)
.with("LoadBalancerFullName", lb_full)
.with("LoadBalancerName", name)
.with("DNSName", dns_name)
.with("CanonicalHostedZoneID", canonical))
}
fn update_elbv2_target_group(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let arn = existing.physical_id.clone();
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let tg = state
.target_groups
.get_mut(&arn)
.ok_or_else(|| format!("TargetGroup {arn} no longer exists"))?;
if let Some(s) = props.get("HealthCheckProtocol").and_then(|v| v.as_str()) {
tg.health_check_protocol = Some(s.to_string());
}
if let Some(s) = props.get("HealthCheckPort").and_then(|v| v.as_str()) {
tg.health_check_port = Some(s.to_string());
}
if let Some(b) = props.get("HealthCheckEnabled").and_then(|v| v.as_bool()) {
tg.health_check_enabled = b;
}
if let Some(s) = props.get("HealthCheckPath").and_then(|v| v.as_str()) {
tg.health_check_path = Some(s.to_string());
}
if let Some(n) = props.get("HealthCheckIntervalSeconds").and_then(cfn_as_i64) {
tg.health_check_interval_seconds = n as i32;
}
if let Some(n) = props.get("HealthCheckTimeoutSeconds").and_then(cfn_as_i64) {
tg.health_check_timeout_seconds = n as i32;
}
if let Some(n) = props.get("HealthyThresholdCount").and_then(cfn_as_i64) {
tg.healthy_threshold_count = n as i32;
}
if let Some(n) = props.get("UnhealthyThresholdCount").and_then(cfn_as_i64) {
tg.unhealthy_threshold_count = n as i32;
}
if let Some(matcher) = props.get("Matcher") {
tg.matcher_http_code = matcher
.get("HttpCode")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
tg.matcher_grpc_code = matcher
.get("GrpcCode")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
}
if props.get("Tags").is_some() {
tg.tags = parse_elb_tags(props.get("Tags"));
}
let name = tg.name.clone();
let tg_full = arn
.rsplit("targetgroup/")
.next()
.map(|s| format!("targetgroup/{s}"))
.unwrap_or_default();
Ok(ProvisionResult::new(arn.clone())
.with("TargetGroupArn", arn)
.with("TargetGroupName", name)
.with("TargetGroupFullName", tg_full))
}
fn update_elbv2_listener(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let arn = existing.physical_id.clone();
let new_default_actions = props
.get("DefaultActions")
.map(|v| parse_elb_actions(Some(v)));
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let listener = state
.listeners
.get_mut(&arn)
.ok_or_else(|| format!("Listener {arn} no longer exists"))?;
if let Some(n) = props.get("Port").and_then(cfn_as_i64) {
listener.port = Some(n as i32);
}
if let Some(s) = props.get("Protocol").and_then(|v| v.as_str()) {
listener.protocol = Some(s.to_string());
}
if let Some(s) = props.get("SslPolicy").and_then(|v| v.as_str()) {
listener.ssl_policy = Some(s.to_string());
}
if let Some(actions) = new_default_actions {
listener.default_actions = actions;
}
if props.get("Tags").is_some() {
listener.tags = parse_elb_tags(props.get("Tags"));
}
Ok(ProvisionResult::new(arn.clone()).with("ListenerArn", arn))
}
fn update_elbv2_listener_rule(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let arn = existing.physical_id.clone();
let new_actions = props.get("Actions").map(|v| parse_elb_actions(Some(v)));
let new_conditions = props
.get("Conditions")
.map(|v| parse_elb_rule_conditions(Some(v)));
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let rule = state
.rules
.get_mut(&arn)
.ok_or_else(|| format!("ListenerRule {arn} no longer exists"))?;
if let Some(v) = props.get("Priority") {
rule.priority = if let Some(s) = v.as_str() {
s.to_string()
} else if let Some(n) = v.as_i64() {
n.to_string()
} else {
rule.priority.clone()
};
}
if let Some(actions) = new_actions {
rule.actions = actions;
}
if let Some(conditions) = new_conditions {
rule.conditions = conditions;
}
if props.get("Tags").is_some() {
rule.tags = parse_elb_tags(props.get("Tags"));
}
Ok(ProvisionResult::new(arn.clone()).with("RuleArn", arn))
}
fn update_elbv2_listener_certificate(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let physical_id = existing.physical_id.clone();
let listener_arn = props
.get("ListenerArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| physical_id.split_once('#').map(|(l, _)| l.to_string()))
.ok_or_else(|| "ListenerArn is required".to_string())?;
let new_certs: Vec<String> = props
.get("Certificates")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|c| c.get("CertificateArn").and_then(|v| v.as_str()))
.map(|s| s.to_string())
.collect()
})
.unwrap_or_default();
if new_certs.is_empty() {
return Err("Certificates must contain at least one CertificateArn".to_string());
}
let prev_certs: Vec<String> = physical_id
.split_once('#')
.map(|(_, list)| list.split(',').map(|s| s.to_string()).collect())
.unwrap_or_default();
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let listener = state
.listeners
.get_mut(&listener_arn)
.ok_or_else(|| format!("Listener {listener_arn} does not exist"))?;
listener
.certificates
.retain(|c| !prev_certs.iter().any(|p| p == &c.certificate_arn));
for arn in &new_certs {
listener.certificates.retain(|c| &c.certificate_arn != arn);
listener.certificates.push(fakecloud_elbv2::Certificate {
certificate_arn: arn.clone(),
is_default: false,
});
}
Ok(ProvisionResult::new(format!(
"{}#{}",
listener_arn,
new_certs.join(",")
)))
}
fn update_elbv2_trust_store(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let arn = existing.physical_id.clone();
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let ts = state
.trust_stores
.get_mut(&arn)
.ok_or_else(|| format!("TrustStore {arn} no longer exists"))?;
let new_bucket = props
.get("CaCertificatesBundleS3Bucket")
.and_then(|v| v.as_str());
let new_key = props
.get("CaCertificatesBundleS3Key")
.and_then(|v| v.as_str());
if let (Some(b), Some(k)) = (new_bucket, new_key) {
ts.ca_certificates_bundle = Some(format!("s3://{b}/{k}").into_bytes());
}
if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
ts.tags = arr
.iter()
.filter_map(|t| {
let k = t.get("Key").and_then(|v| v.as_str())?;
let v = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
Some(fakecloud_elbv2::Tag {
key: k.to_string(),
value: v.to_string(),
})
})
.collect();
}
let name = ts.name.clone();
let status = ts.status.clone();
Ok(ProvisionResult::new(arn.clone())
.with("TrustStoreArn", arn)
.with("Name", name)
.with("Status", status))
}
fn get_att_elbv2_load_balancer(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let lb = state.load_balancers.get(physical_id)?;
let lb_full = lb
.arn
.rsplit("loadbalancer/")
.next()
.unwrap_or("")
.to_string();
match attribute {
"Arn" | "LoadBalancerArn" => Some(lb.arn.clone()),
"DNSName" => Some(lb.dns_name.clone()),
"CanonicalHostedZoneID" => Some(lb.canonical_hosted_zone_id.clone()),
"LoadBalancerFullName" => Some(lb_full),
"LoadBalancerName" => Some(lb.name.clone()),
"SecurityGroups" => Some(lb.security_groups.join(",")),
_ => None,
}
}
fn get_att_elbv2_target_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let tg = state.target_groups.get(physical_id)?;
let tg_full = tg
.arn
.rsplit("targetgroup/")
.next()
.map(|s| format!("targetgroup/{s}"))
.unwrap_or_default();
match attribute {
"TargetGroupArn" => Some(tg.arn.clone()),
"TargetGroupName" => Some(tg.name.clone()),
"TargetGroupFullName" => Some(tg_full),
"LoadBalancerArns" => Some(tg.load_balancer_arns.join(",")),
_ => None,
}
}
fn get_att_elbv2_listener(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let listener = state.listeners.get(physical_id)?;
match attribute {
"Arn" | "ListenerArn" => Some(listener.arn.clone()),
_ => None,
}
}
fn get_att_elbv2_listener_rule(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let rule = state.rules.get(physical_id)?;
match attribute {
"RuleArn" => Some(rule.arn.clone()),
"IsDefault" => Some(rule.is_default.to_string()),
_ => None,
}
}
fn get_att_elbv2_trust_store(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.elbv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let ts = state.trust_stores.get(physical_id)?;
match attribute {
"TrustStoreArn" => Some(ts.arn.clone()),
"Name" => Some(ts.name.clone()),
"Status" => Some(ts.status.clone()),
"NumberOfCaCertificates" => Some(ts.number_of_ca_certificates.to_string()),
"TotalRevokedEntries" => Some(ts.total_revoked_entries.to_string()),
_ => None,
}
}
fn create_organization(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let feature_set = props
.get("FeatureSet")
.and_then(|v| v.as_str())
.unwrap_or("ALL")
.to_string();
let mut org = self.organizations_state.write();
if org.is_some() {
return Err("Organization already exists; only one per fakecloud process".to_string());
}
let mut state = OrganizationState::bootstrap(&self.account_id);
state.feature_set = feature_set;
let org_id = state.org_id.clone();
let org_arn = state.org_arn.clone();
let mgmt_arn = state.management_account_arn.clone();
let root_id = state.root_id.clone();
*org = Some(state);
Ok(ProvisionResult::new(org_id.clone())
.with("Id", org_id)
.with("Arn", org_arn)
.with("ManagementAccountArn", mgmt_arn)
.with("RootId", root_id))
}
fn delete_organization(&self, _physical_id: &str) -> Result<(), String> {
let mut org = self.organizations_state.write();
*org = None;
Ok(())
}
fn create_organization_unit(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let parent_id = props
.get("ParentId")
.and_then(|v| v.as_str())
.ok_or_else(|| "ParentId is required".to_string())?
.to_string();
let mut org_lock = self.organizations_state.write();
let org = org_lock
.as_mut()
.ok_or_else(|| "Organization not yet created".to_string())?;
let resolved_parent_id = if parent_id == org.root_id || org.ous.contains_key(&parent_id) {
parent_id
} else {
return Err(format!("Parent {parent_id} does not exist"));
};
let id_suffix: String = Uuid::new_v4()
.simple()
.to_string()
.chars()
.take(8)
.collect();
let id = format!("ou-{}-{}", &org.root_id[2..], id_suffix);
let arn = format!(
"arn:aws:organizations::{}:ou/{}/{}",
org.management_account_id, org.org_id, id
);
org.ous.insert(
id.clone(),
OrganizationalUnit {
id: id.clone(),
arn: arn.clone(),
name: name.clone(),
parent_id: resolved_parent_id,
},
);
Ok(ProvisionResult::new(id.clone())
.with("Id", id)
.with("Arn", arn)
.with("Name", name))
}
fn delete_organization_unit(&self, physical_id: &str) -> Result<(), String> {
let mut org_lock = self.organizations_state.write();
if let Some(org) = org_lock.as_mut() {
org.ous.remove(physical_id);
org.attachments.remove(physical_id);
}
Ok(())
}
fn create_organization_account(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let email = props
.get("Email")
.and_then(|v| v.as_str())
.ok_or_else(|| "Email is required".to_string())?
.to_string();
let name = props
.get("AccountName")
.and_then(|v| v.as_str())
.ok_or_else(|| "AccountName is required".to_string())?
.to_string();
let parent_ids: Vec<String> = props
.get("ParentIds")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let tags: Vec<(String, String)> = props
.get("Tags")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|t| {
let k = t.get("Key").and_then(|v| v.as_str())?;
let val = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
Some((k.to_string(), val.to_string()))
})
.collect()
})
.unwrap_or_default();
let mut org_lock = self.organizations_state.write();
let org = org_lock
.as_mut()
.ok_or_else(|| "Organization not yet created".to_string())?;
let pending = org.begin_create_account(&email, &name, None);
let status = org.complete_create_account(&pending.id).unwrap_or(pending);
let account_id = status
.account_id
.clone()
.ok_or_else(|| "create_account did not return an account id".to_string())?;
let account_arn = org
.accounts
.get(&account_id)
.map(|a| a.arn.clone())
.unwrap_or_default();
let joined_method = org
.accounts
.get(&account_id)
.map(|a| a.joined_method.clone())
.unwrap_or_else(|| "CREATED".to_string());
let joined_timestamp = org
.accounts
.get(&account_id)
.map(|a| a.joined_timestamp.to_rfc3339())
.unwrap_or_default();
let acct_status = org
.accounts
.get(&account_id)
.map(|a| a.status.clone())
.unwrap_or_else(|| "ACTIVE".to_string());
if let Some(parent) = parent_ids.first() {
let source = org
.accounts
.get(&account_id)
.map(|a| a.parent_id.clone())
.unwrap_or_else(|| org.root_id.clone());
if parent != &source {
org.move_account(&account_id, &source, parent)
.map_err(|e| format!("Failed to move account to parent {parent}: {e:?}"))?;
}
}
if !tags.is_empty() {
org.set_resource_tags(&account_id, &tags);
}
Ok(ProvisionResult::new(account_id.clone())
.with("AccountId", account_id)
.with("AccountName", name)
.with("Email", email)
.with("Arn", account_arn)
.with("JoinedMethod", joined_method)
.with("JoinedTimestamp", joined_timestamp)
.with("Status", acct_status))
}
fn delete_organization_account(&self, physical_id: &str) -> Result<(), String> {
let mut org_lock = self.organizations_state.write();
if let Some(org) = org_lock.as_mut() {
let _ = org.close_account(physical_id);
}
Ok(())
}
fn create_organization_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let policy_type = props
.get("Type")
.and_then(|v| v.as_str())
.unwrap_or(POLICY_TYPE_SCP)
.to_string();
let content = props
.get("Content")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.unwrap_or_default();
let target_ids: Vec<String> = props
.get("TargetIds")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|t| t.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let mut org_lock = self.organizations_state.write();
let org = org_lock
.as_mut()
.ok_or_else(|| "Organization not yet created".to_string())?;
let id_suffix: String = Uuid::new_v4()
.simple()
.to_string()
.chars()
.take(8)
.collect();
let id = format!("p-{}", id_suffix);
let arn = format!(
"arn:aws:organizations::{}:policy/{}/{}/{}",
org.management_account_id,
org.org_id,
policy_type.to_lowercase(),
id
);
org.policies.insert(
id.clone(),
OrgPolicy {
id: id.clone(),
arn: arn.clone(),
name: name.clone(),
description,
policy_type,
aws_managed: false,
content,
},
);
for target in target_ids {
org.attachments
.entry(target)
.or_default()
.insert(id.clone());
}
Ok(ProvisionResult::new(id.clone())
.with("Id", id)
.with("Arn", arn)
.with("Name", name))
}
fn delete_organization_policy(&self, physical_id: &str) -> Result<(), String> {
let mut org_lock = self.organizations_state.write();
if let Some(org) = org_lock.as_mut() {
org.policies.remove(physical_id);
for attachments in org.attachments.values_mut() {
attachments.remove(physical_id);
}
}
Ok(())
}
fn create_organization_resource_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let content = props
.get("Content")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "Content is required".to_string())?;
let mut org_lock = self.organizations_state.write();
let org = org_lock
.as_mut()
.ok_or_else(|| "Organization not yet created".to_string())?;
org.resource_policy = Some(content);
let arn = format!(
"arn:aws:organizations::{}:resourcepolicy/{}/rp",
org.management_account_id, org.org_id
);
Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
}
fn delete_organization_resource_policy(&self, _physical_id: &str) -> Result<(), String> {
let mut org_lock = self.organizations_state.write();
if let Some(org) = org_lock.as_mut() {
org.resource_policy = None;
}
Ok(())
}
fn delete_log_group(&self, physical_id: &str) -> Result<(), String> {
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.default_mut();
let name = state
.log_groups
.iter()
.find(|(_, g)| g.arn == physical_id)
.map(|(name, _)| name.clone());
if let Some(name) = name {
state.log_groups.remove(&name);
}
Ok(())
}
fn create_log_stream(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let log_group_name = props
.get("LogGroupName")
.and_then(|v| v.as_str())
.map(parse_log_group_name)
.ok_or_else(|| "LogGroupName is required".to_string())?;
let log_stream_name = props
.get("LogStreamName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
let group = state
.log_groups
.get_mut(&log_group_name)
.ok_or_else(|| format!("Log group {log_group_name} does not exist"))?;
let arn = format!(
"arn:aws:logs:{}:{}:log-group:{}:log-stream:{}",
self.region, self.account_id, log_group_name, log_stream_name
);
if group.log_streams.contains_key(&log_stream_name) {
return Err(format!(
"Log stream {log_stream_name} already exists in {log_group_name}"
));
}
group.log_streams.insert(
log_stream_name.clone(),
LogStream {
name: log_stream_name.clone(),
arn,
creation_time: Utc::now().timestamp_millis(),
first_event_timestamp: None,
last_event_timestamp: None,
last_ingestion_time: None,
upload_sequence_token: String::new(),
events: Vec::new(),
},
);
let physical_id = format!("{log_group_name}|{log_stream_name}");
Ok(ProvisionResult::new(physical_id))
}
fn delete_log_stream(&self, physical_id: &str) -> Result<(), String> {
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
if let Some((group_name, stream_name)) = physical_id.split_once('|') {
if let Some(group) = state.log_groups.get_mut(group_name) {
group.log_streams.remove(stream_name);
}
}
Ok(())
}
fn create_metric_filter(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let log_group_name = props
.get("LogGroupName")
.and_then(|v| v.as_str())
.map(parse_log_group_name)
.ok_or_else(|| "LogGroupName is required".to_string())?;
let filter_name = props
.get("FilterName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let filter_pattern = props
.get("FilterPattern")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let mut transformations: Vec<MetricTransformation> = Vec::new();
if let Some(arr) = props
.get("MetricTransformations")
.and_then(|v| v.as_array())
{
for t in arr {
let metric_name = t
.get("MetricName")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let metric_namespace = t
.get("MetricNamespace")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let metric_value = t
.get("MetricValue")
.and_then(|v| v.as_str())
.unwrap_or("1")
.to_string();
let default_value = t.get("DefaultValue").and_then(|v| v.as_f64());
transformations.push(MetricTransformation {
metric_name,
metric_namespace,
metric_value,
default_value,
});
}
}
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
if !state.log_groups.contains_key(&log_group_name) {
return Err(format!("Log group {log_group_name} does not exist"));
}
state
.metric_filters
.retain(|f| !(f.log_group_name == log_group_name && f.filter_name == filter_name));
state.metric_filters.push(MetricFilter {
filter_name: filter_name.clone(),
filter_pattern,
log_group_name: log_group_name.clone(),
metric_transformations: transformations,
creation_time: Utc::now().timestamp_millis(),
});
Ok(ProvisionResult::new(format!(
"{log_group_name}|{filter_name}"
)))
}
fn delete_metric_filter(&self, physical_id: &str) -> Result<(), String> {
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
if let Some((group_name, filter_name)) = physical_id.split_once('|') {
state
.metric_filters
.retain(|f| !(f.log_group_name == group_name && f.filter_name == filter_name));
}
Ok(())
}
fn create_subscription_filter(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let log_group_name = props
.get("LogGroupName")
.and_then(|v| v.as_str())
.map(parse_log_group_name)
.ok_or_else(|| "LogGroupName is required".to_string())?;
let filter_name = props
.get("FilterName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let filter_pattern = props
.get("FilterPattern")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let destination_arn = props
.get("DestinationArn")
.and_then(|v| v.as_str())
.ok_or_else(|| "DestinationArn is required".to_string())?
.to_string();
let role_arn = props
.get("RoleArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let distribution = props
.get("Distribution")
.and_then(|v| v.as_str())
.unwrap_or("ByLogStream")
.to_string();
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
let group = state
.log_groups
.get_mut(&log_group_name)
.ok_or_else(|| format!("Log group {log_group_name} does not exist"))?;
group
.subscription_filters
.retain(|f| f.filter_name != filter_name);
group.subscription_filters.push(SubscriptionFilter {
filter_name: filter_name.clone(),
log_group_name: log_group_name.clone(),
filter_pattern,
destination_arn,
role_arn,
distribution,
creation_time: Utc::now().timestamp_millis(),
});
Ok(ProvisionResult::new(format!(
"{log_group_name}|{filter_name}"
)))
}
fn delete_subscription_filter(&self, physical_id: &str) -> Result<(), String> {
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
if let Some((group_name, filter_name)) = physical_id.split_once('|') {
if let Some(group) = state.log_groups.get_mut(group_name) {
group
.subscription_filters
.retain(|f| f.filter_name != filter_name);
}
}
Ok(())
}
fn create_logs_destination(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let destination_name = props
.get("DestinationName")
.and_then(|v| v.as_str())
.ok_or("DestinationName is required")?
.to_string();
let target_arn = props
.get("TargetArn")
.and_then(|v| v.as_str())
.ok_or("TargetArn is required")?
.to_string();
let role_arn = props
.get("RoleArn")
.and_then(|v| v.as_str())
.ok_or("RoleArn is required")?
.to_string();
let access_policy = props
.get("DestinationPolicy")
.and_then(|v| v.as_str())
.map(String::from);
let arn = format!(
"arn:aws:logs:{}:{}:destination:{destination_name}",
self.region, self.account_id
);
let dest = Destination {
destination_name: destination_name.clone(),
target_arn,
role_arn,
arn: arn.clone(),
access_policy,
creation_time: Utc::now().timestamp_millis(),
tags: BTreeMap::new(),
};
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
state.destinations.insert(destination_name.clone(), dest);
Ok(ProvisionResult::new(destination_name).with("Arn", arn))
}
fn delete_logs_destination(&self, physical_id: &str) -> Result<(), String> {
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
state.destinations.remove(physical_id);
Ok(())
}
fn create_logs_resource_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let policy_name = props
.get("PolicyName")
.and_then(|v| v.as_str())
.ok_or("PolicyName is required")?
.to_string();
let policy_document = props
.get("PolicyDocument")
.map(|v| {
if let Some(s) = v.as_str() {
s.to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or("PolicyDocument is required")?;
let policy = ResourcePolicy {
policy_name: policy_name.clone(),
policy_document,
last_updated_time: Utc::now().timestamp_millis(),
};
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
state.resource_policies.insert(policy_name.clone(), policy);
Ok(ProvisionResult::new(policy_name))
}
fn delete_logs_resource_policy(&self, physical_id: &str) -> Result<(), String> {
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
state.resource_policies.remove(physical_id);
Ok(())
}
fn create_logs_query_definition(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let query_string = props
.get("QueryString")
.and_then(|v| v.as_str())
.ok_or("QueryString is required")?
.to_string();
let log_group_names: Vec<String> = props
.get("LogGroupNames")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let id = Uuid::new_v4().to_string();
let qd = QueryDefinition {
query_definition_id: id.clone(),
name,
query_string,
log_group_names,
last_modified: Utc::now().timestamp_millis(),
};
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
state.query_definitions.insert(id.clone(), qd);
Ok(ProvisionResult::new(id.clone()).with("QueryDefinitionId", id))
}
fn delete_logs_query_definition(&self, physical_id: &str) -> Result<(), String> {
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
state.query_definitions.remove(physical_id);
Ok(())
}
fn create_logs_delivery_destination(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let output_format = props
.get("OutputFormat")
.and_then(|v| v.as_str())
.map(String::from);
let mut configuration: BTreeMap<String, String> = BTreeMap::new();
if let Some(arn) = props.get("DestinationResourceArn").and_then(|v| v.as_str()) {
configuration.insert("destinationResourceArn".to_string(), arn.to_string());
}
if let Some(cfg) = props
.get("DeliveryDestinationConfiguration")
.and_then(|v| v.as_object())
{
for (k, v) in cfg {
if let Some(s) = v.as_str() {
configuration.insert(k.clone(), s.to_string());
}
}
}
let policy = props.get("DeliveryDestinationPolicy").map(|v| {
if let Some(s) = v.as_str() {
s.to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
});
let arn = format!(
"arn:aws:logs:{}:{}:delivery-destination:{name}",
self.region, self.account_id
);
let dd = DeliveryDestination {
name: name.clone(),
arn: arn.clone(),
output_format,
delivery_destination_configuration: configuration,
tags: BTreeMap::new(),
delivery_destination_policy: policy,
};
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
state.delivery_destinations.insert(name.clone(), dd);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_logs_delivery_destination(&self, physical_id: &str) -> Result<(), String> {
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
state.delivery_destinations.remove(physical_id);
Ok(())
}
fn create_logs_delivery_source(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let resource_arns: Vec<String> = props
.get("ResourceArn")
.and_then(|v| v.as_str())
.map(|s| vec![s.to_string()])
.or_else(|| {
props
.get("ResourceArns")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
})
.unwrap_or_default();
let log_type = props
.get("LogType")
.and_then(|v| v.as_str())
.ok_or("LogType is required")?
.to_string();
let service = props
.get("Service")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let arn = format!(
"arn:aws:logs:{}:{}:delivery-source:{name}",
self.region, self.account_id
);
let ds = DeliverySource {
name: name.clone(),
arn: arn.clone(),
resource_arns,
service,
log_type,
tags: BTreeMap::new(),
created_at: chrono::Utc::now().timestamp_millis(),
};
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
state.delivery_sources.insert(name.clone(), ds);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_logs_delivery_source(&self, physical_id: &str) -> Result<(), String> {
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
state.delivery_sources.remove(physical_id);
Ok(())
}
fn create_logs_delivery(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let delivery_source_name = props
.get("DeliverySourceName")
.and_then(|v| v.as_str())
.ok_or("DeliverySourceName is required")?
.to_string();
let delivery_destination_arn = props
.get("DeliveryDestinationArn")
.and_then(|v| v.as_str())
.ok_or("DeliveryDestinationArn is required")?
.to_string();
let delivery_destination_type = if delivery_destination_arn.contains(":s3:") {
"S3".to_string()
} else if delivery_destination_arn.contains(":firehose:") {
"FH".to_string()
} else {
"CWL".to_string()
};
let id = Uuid::new_v4().simple().to_string();
let arn = format!(
"arn:aws:logs:{}:{}:delivery:{id}",
self.region, self.account_id
);
let delivery = Delivery {
id: id.clone(),
delivery_source_name,
delivery_destination_arn,
delivery_destination_type,
arn: arn.clone(),
tags: BTreeMap::new(),
field_delimiter: None,
record_fields: Vec::new(),
s3_delivery_configuration: None,
created_at: chrono::Utc::now().timestamp_millis(),
};
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
state.deliveries.insert(id.clone(), delivery);
Ok(ProvisionResult::new(id.clone())
.with("DeliveryId", id)
.with("Arn", arn))
}
fn delete_logs_delivery(&self, physical_id: &str) -> Result<(), String> {
let mut logs_accounts = self.logs_state.write();
let state = logs_accounts.get_or_create(&self.account_id);
state.deliveries.remove(physical_id);
Ok(())
}
fn invoke_lambda_sync(&self, function_arn: &str, payload: &str) -> Result<(), String> {
let delivery = self.delivery.clone();
let function_arn = function_arn.to_string();
let payload = payload.to_string();
std::thread::scope(|s| {
s.spawn(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("Failed to create runtime: {e}"))?;
rt.block_on(async {
match delivery.invoke_lambda(&function_arn, &payload).await {
Some(Ok(_)) => {
tracing::info!(
"Custom resource Lambda {} invoked successfully",
function_arn
);
Ok(())
}
Some(Err(e)) => {
tracing::warn!(
"Custom resource Lambda {} invocation failed: {e}",
function_arn
);
Err(format!("Lambda invocation failed: {e}"))
}
None => {
tracing::warn!(
"No Lambda delivery configured; skipping custom resource invocation for {}",
function_arn
);
Ok(())
}
}
})
})
.join()
.map_err(|_| "Lambda invocation thread panicked".to_string())?
})
}
fn create_custom_resource(&self, resource: &ResourceDefinition) -> Result<String, String> {
let props = &resource.properties;
let service_token = props
.get("ServiceToken")
.and_then(|v| v.as_str())
.ok_or("Custom resource requires ServiceToken property")?;
let request_id = Uuid::new_v4().to_string();
let event = serde_json::json!({
"RequestType": "Create",
"ServiceToken": service_token,
"StackId": self.stack_id,
"RequestId": request_id,
"ResourceType": resource.resource_type,
"LogicalResourceId": resource.logical_id,
"ResourceProperties": props,
});
let payload = serde_json::to_string(&event).map_err(|e| e.to_string())?;
self.invoke_lambda_sync(service_token, &payload)?;
let physical_id = format!("{}-{}", resource.logical_id, &request_id[..8]);
Ok(physical_id)
}
fn delete_custom_resource(&self, resource: &StackResource) -> Result<(), String> {
let service_token = match &resource.service_token {
Some(token) => token.clone(),
None => {
return Ok(());
}
};
let request_id = Uuid::new_v4().to_string();
let event = serde_json::json!({
"RequestType": "Delete",
"ServiceToken": service_token,
"StackId": self.stack_id,
"RequestId": request_id,
"ResourceType": resource.resource_type,
"LogicalResourceId": resource.logical_id,
"PhysicalResourceId": resource.physical_id,
});
let payload = serde_json::to_string(&event).map_err(|e| e.to_string())?;
if let Err(e) = self.invoke_lambda_sync(&service_token, &payload) {
tracing::warn!(
"Custom resource delete Lambda invocation failed for {}: {e}",
resource.logical_id
);
}
Ok(())
}
fn create_application_autoscaling_scalable_target(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let service_namespace = props
.get("ServiceNamespace")
.and_then(|v| v.as_str())
.ok_or_else(|| "ServiceNamespace is required".to_string())?
.to_string();
let resource_id = props
.get("ResourceId")
.and_then(|v| v.as_str())
.ok_or_else(|| "ResourceId is required".to_string())?
.to_string();
let scalable_dimension = props
.get("ScalableDimension")
.and_then(|v| v.as_str())
.ok_or_else(|| "ScalableDimension is required".to_string())?
.to_string();
let min_capacity = props
.get("MinCapacity")
.and_then(|v| v.as_i64())
.map(|n| n as i32)
.ok_or_else(|| "MinCapacity is required".to_string())?;
let max_capacity = props
.get("MaxCapacity")
.and_then(|v| v.as_i64())
.map(|n| n as i32)
.ok_or_else(|| "MaxCapacity is required".to_string())?;
if min_capacity > max_capacity {
return Err("MinCapacity must be <= MaxCapacity".to_string());
}
let role_arn = props
.get("RoleARN")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let suspended_state = props.get("SuspendedState").map(|v| AppasSuspendedState {
dynamic_scaling_in_suspended: v
.get("DynamicScalingInSuspended")
.and_then(|x| x.as_bool()),
dynamic_scaling_out_suspended: v
.get("DynamicScalingOutSuspended")
.and_then(|x| x.as_bool()),
scheduled_scaling_suspended: v
.get("ScheduledScalingSuspended")
.and_then(|x| x.as_bool()),
});
let arn = format!(
"arn:aws:application-autoscaling:{}:{}:scalable-target/{}",
self.region,
self.account_id,
&Uuid::new_v4().simple().to_string()[..10]
);
let role = role_arn.unwrap_or_else(|| {
let suffix = match service_namespace.as_str() {
"ecs" => "ECSService",
"elasticmapreduce" => "EMRContainerService",
"ec2" => "EC2SpotFleetRequest",
"appstream" => "ApplicationAutoScaling_AppStreamFleet",
"dynamodb" => "DynamoDBTable",
"rds" => "RDSCluster",
"sagemaker" => "SageMakerEndpoint",
"lambda" => "LambdaConcurrency",
"elasticache" => "ElastiCacheRG",
"cassandra" => "CassandraTable",
"kafka" => "KafkaCluster",
_ => "ApplicationAutoScaling_Default",
};
format!(
"arn:aws:iam::{}:role/aws-service-role/applicationautoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_{}",
self.account_id, suffix
)
});
let mut state = self.app_autoscaling_state.write();
let account = state.accounts.entry(self.account_id.clone()).or_default();
let key = (
service_namespace.clone(),
resource_id.clone(),
scalable_dimension.clone(),
);
let target = AppasScalableTarget {
arn: arn.clone(),
service_namespace: service_namespace.clone(),
resource_id: resource_id.clone(),
scalable_dimension: scalable_dimension.clone(),
min_capacity,
max_capacity,
role_arn: role,
creation_time: Utc::now(),
suspended_state,
predicted_capacity: None,
};
account.scalable_targets.insert(key, target);
Ok(ProvisionResult::new(resource_id.clone())
.with("ScalableTargetARN", arn)
.with("ServiceNamespace", service_namespace)
.with("ScalableDimension", scalable_dimension))
}
fn create_application_autoscaling_scaling_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let policy_name = props
.get("PolicyName")
.and_then(|v| v.as_str())
.ok_or_else(|| "PolicyName is required".to_string())?
.to_string();
let service_namespace = props
.get("ServiceNamespace")
.and_then(|v| v.as_str())
.ok_or_else(|| "ServiceNamespace is required".to_string())?
.to_string();
let resource_id = props
.get("ResourceId")
.and_then(|v| v.as_str())
.ok_or_else(|| "ResourceId is required".to_string())?
.to_string();
let scalable_dimension = props
.get("ScalableDimension")
.and_then(|v| v.as_str())
.ok_or_else(|| "ScalableDimension is required".to_string())?
.to_string();
let policy_type = props
.get("PolicyType")
.and_then(|v| v.as_str())
.unwrap_or("StepScaling")
.to_string();
let step_cfg = props.get("StepScalingPolicyConfiguration").cloned();
let tt_cfg = props
.get("TargetTrackingScalingPolicyConfiguration")
.cloned();
let pred_cfg = props.get("PredictiveScalingPolicyConfiguration").cloned();
let target_key = (
service_namespace.clone(),
resource_id.clone(),
scalable_dimension.clone(),
);
let policy_key = (
service_namespace.clone(),
resource_id.clone(),
scalable_dimension.clone(),
policy_name.clone(),
);
let mut state = self.app_autoscaling_state.write();
let account = state.accounts.entry(self.account_id.clone()).or_default();
if !account.scalable_targets.contains_key(&target_key) {
return Err(format!(
"No scalable target registered for ServiceNamespace={} ResourceId={} ScalableDimension={}",
service_namespace, resource_id, scalable_dimension
));
}
let arn = format!(
"arn:aws:autoscaling:{}:{}:scalingPolicy:{}:resource/{}/{}:policyName/{}",
self.region,
self.account_id,
Uuid::new_v4(),
service_namespace,
resource_id,
policy_name
);
let policy = AppasScalingPolicy {
arn: arn.clone(),
policy_name: policy_name.clone(),
service_namespace: service_namespace.clone(),
resource_id: resource_id.clone(),
scalable_dimension: scalable_dimension.clone(),
policy_type: policy_type.clone(),
creation_time: Utc::now(),
step_scaling_policy_configuration: step_cfg,
target_tracking_scaling_policy_configuration: tt_cfg,
predictive_scaling_policy_configuration: pred_cfg,
alarms: Vec::new(),
last_applied_at: None,
};
account.scaling_policies.insert(policy_key, policy);
Ok(ProvisionResult::new(arn.clone())
.with("PolicyName", policy_name)
.with("ServiceNamespace", service_namespace)
.with("ResourceId", resource_id)
.with("ScalableDimension", scalable_dimension))
}
fn delete_application_autoscaling_scalable_target(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let namespace = attributes
.get("ServiceNamespace")
.cloned()
.ok_or_else(|| "ServiceNamespace missing in attributes".to_string())?;
let resource_id = physical_id.to_string();
let dimension = attributes
.get("ScalableDimension")
.cloned()
.ok_or_else(|| "ScalableDimension missing in attributes".to_string())?;
let key = (namespace, resource_id.clone(), dimension);
let mut state = self.app_autoscaling_state.write();
let account = state.accounts.entry(self.account_id.clone()).or_default();
account.scalable_targets.remove(&key);
account
.scaling_policies
.retain(|k, _| !(k.0 == key.0 && k.1 == key.1 && k.2 == key.2));
account
.scheduled_actions
.retain(|k, _| !(k.0 == key.0 && k.1 == key.1 && k.2 == key.2));
Ok(())
}
fn delete_application_autoscaling_scaling_policy(
&self,
_physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let policy_name = attributes
.get("PolicyName")
.cloned()
.ok_or_else(|| "PolicyName missing in attributes".to_string())?;
let namespace = attributes
.get("ServiceNamespace")
.cloned()
.ok_or_else(|| "ServiceNamespace missing in attributes".to_string())?;
let resource_id = attributes
.get("ResourceId")
.cloned()
.ok_or_else(|| "ResourceId missing in attributes".to_string())?;
let dimension = attributes
.get("ScalableDimension")
.cloned()
.ok_or_else(|| "ScalableDimension missing in attributes".to_string())?;
let key = (namespace, resource_id, dimension, policy_name);
let mut state = self.app_autoscaling_state.write();
let account = state.accounts.entry(self.account_id.clone()).or_default();
account.scaling_policies.remove(&key);
Ok(())
}
fn create_firehose_delivery_stream(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("DeliveryStreamName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let arn = format!(
"arn:aws:firehose:{}:{}:deliverystream/{}",
self.region, self.account_id, name
);
let stream_type = props
.get("DeliveryStreamType")
.and_then(|v| v.as_str())
.unwrap_or("DirectPut")
.to_string();
let has_s3 = props.get("S3DestinationConfiguration").is_some();
let has_extended_s3 = props.get("ExtendedS3DestinationConfiguration").is_some();
if has_s3 && has_extended_s3 {
return Err("Only one of S3DestinationConfiguration or ExtendedS3DestinationConfiguration may be set".to_string());
}
let destination = Some(if let Some(s3) = props.get("S3DestinationConfiguration") {
parse_firehose_s3_destination(s3)?
} else if let Some(s3) = props.get("ExtendedS3DestinationConfiguration") {
parse_firehose_s3_destination(s3)?
} else {
return Err("Delivery stream requires a destination configuration".to_string());
});
let mut tags = BTreeMap::new();
if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
for tag in arr {
if let (Some(k), Some(v)) = (
tag.get("Key").and_then(|v| v.as_str()),
tag.get("Value").and_then(|v| v.as_str()),
) {
tags.insert(k.to_string(), v.to_string());
}
}
}
let stream = DeliveryStream {
name: name.clone(),
arn: arn.clone(),
status: "ACTIVE".to_string(),
stream_type: stream_type.clone(),
created_at: Utc::now(),
last_update: Utc::now(),
version_id: "1".to_string(),
destination,
tags,
};
let mut state = self.firehose_state.write();
let account = state.get_or_create(&self.account_id, &self.region);
account
.streams_mut(&self.region)
.insert(name.clone(), stream);
let mut attributes = BTreeMap::new();
attributes.insert("Arn".to_string(), arn.clone());
attributes.insert("DeliveryStreamName".to_string(), name.clone());
Ok(ProvisionResult {
physical_id: name,
attributes,
})
}
fn delete_firehose_delivery_stream(&self, physical_id: &str) -> Result<(), String> {
let mut state = self.firehose_state.write();
let account = state.get_or_create(&self.account_id, &self.region);
account.streams_mut(&self.region).remove(physical_id);
Ok(())
}
fn create_cognito_user_pool(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let pool_name = props
.get("PoolName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let pool_id = format!(
"{}_{}",
self.region,
Uuid::new_v4()
.simple()
.to_string()
.chars()
.take(9)
.collect::<String>()
);
let arn = format!(
"arn:aws:cognito-idp:{}:{}:userpool/{}",
self.region, self.account_id, pool_id
);
let now = Utc::now();
let password_policy = parse_cognito_password_policy(props.get("Policies"));
let auto_verified = parse_cognito_string_array(props.get("AutoVerifiedAttributes"));
let username_attributes = props
.get("UsernameAttributes")
.and_then(|v| v.as_array())
.map(|_| parse_cognito_string_array(props.get("UsernameAttributes")));
let alias_attributes = props
.get("AliasAttributes")
.and_then(|v| v.as_array())
.map(|_| parse_cognito_string_array(props.get("AliasAttributes")));
let mut schema_attributes = default_schema_attributes();
if let Some(arr) = props.get("Schema").and_then(|v| v.as_array()) {
for attr in arr {
if let Some(parsed) = parse_cognito_schema_attribute(attr) {
if !schema_attributes.iter().any(|a| a.name == parsed.name) {
schema_attributes.push(parsed);
}
}
}
}
let mfa_configuration = props
.get("MfaConfiguration")
.and_then(|v| v.as_str())
.unwrap_or("OFF")
.to_string();
let user_pool_tier = props
.get("UserPoolTier")
.and_then(|v| v.as_str())
.unwrap_or("ESSENTIALS")
.to_string();
let deletion_protection = props
.get("DeletionProtection")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let user_pool_tags = parse_cognito_tags(props.get("UserPoolTags"));
let email_configuration =
parse_cognito_email_configuration(props.get("EmailConfiguration"));
let sms_configuration = parse_cognito_sms_configuration(props.get("SmsConfiguration"));
let admin_create_user_config =
parse_cognito_admin_create_user_config(props.get("AdminCreateUserConfig"));
let account_recovery_setting =
parse_cognito_account_recovery(props.get("AccountRecoverySetting"));
let signing = fakecloud_cognito::jwt::generate_pool_signing_key();
let signing_key_pem = signing.private_key_pem;
let signing_kid = signing.kid;
let pool = UserPool {
id: pool_id.clone(),
name: pool_name,
arn: arn.clone(),
status: "ACTIVE".to_string(),
creation_date: now,
last_modified_date: now,
policies: PoolPolicies {
password_policy,
sign_in_policy: SignInPolicy {
allowed_first_auth_factors: vec!["PASSWORD".to_string()],
},
},
auto_verified_attributes: auto_verified,
username_attributes,
alias_attributes,
schema_attributes,
lambda_config: None,
mfa_configuration,
email_configuration,
sms_configuration,
admin_create_user_config,
user_pool_tags,
account_recovery_setting,
deletion_protection,
estimated_number_of_users: 0,
software_token_mfa_configuration: None,
sms_mfa_configuration: None,
user_pool_tier,
verification_message_template: None,
signing_key_pem: Some(signing_key_pem),
signing_kid: Some(signing_kid),
};
let mut accounts = self.cognito_state.write();
let state = accounts.get_or_create(&self.account_id);
state.user_pools.insert(pool_id.clone(), pool);
let provider_name = format!("cognito-idp.{}.amazonaws.com/{}", self.region, pool_id);
let provider_url = format!("https://{provider_name}");
Ok(ProvisionResult::new(pool_id.clone())
.with("Arn", arn)
.with("ProviderName", provider_name)
.with("ProviderURL", provider_url)
.with("UserPoolId", pool_id))
}
fn delete_cognito_user_pool(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cognito_state.write();
let state = accounts.get_or_create(&self.account_id);
state.user_pools.remove(physical_id);
state
.user_pool_clients
.retain(|_, c| c.user_pool_id != physical_id);
state.users.remove(physical_id);
state.groups.remove(physical_id);
state.user_groups.remove(physical_id);
state.identity_providers.remove(physical_id);
state.resource_servers.remove(physical_id);
state.import_jobs.remove(physical_id);
state.domains.retain(|_, d| d.user_pool_id != physical_id);
Ok(())
}
fn create_cognito_user_pool_client(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let pool_id = props
.get("UserPoolId")
.and_then(|v| v.as_str())
.ok_or_else(|| "UserPoolId is required".to_string())?
.to_string();
let client_name = props
.get("ClientName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let mut accounts = self.cognito_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.user_pools.contains_key(&pool_id) {
return Err(format!(
"User pool {pool_id} does not exist yet — retry once it has been provisioned"
));
}
let client_id: String = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple())
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.take(26)
.collect::<String>()
.to_lowercase();
let generate_secret = props
.get("GenerateSecret")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let client_secret = if generate_secret {
use base64::Engine;
let mut bytes = Vec::with_capacity(48);
for _ in 0..3 {
bytes.extend_from_slice(Uuid::new_v4().as_bytes());
}
Some(
base64::engine::general_purpose::STANDARD
.encode(&bytes)
.chars()
.take(51)
.collect(),
)
} else {
None
};
let now = Utc::now();
let client = UserPoolClient {
client_id: client_id.clone(),
client_name,
user_pool_id: pool_id.clone(),
client_secret: client_secret.clone(),
explicit_auth_flows: parse_cognito_string_array(props.get("ExplicitAuthFlows")),
token_validity_units: None,
access_token_validity: props.get("AccessTokenValidity").and_then(|v| v.as_i64()),
id_token_validity: props.get("IdTokenValidity").and_then(|v| v.as_i64()),
refresh_token_validity: props.get("RefreshTokenValidity").and_then(|v| v.as_i64()),
callback_urls: parse_cognito_string_array(props.get("CallbackURLs")),
logout_urls: parse_cognito_string_array(props.get("LogoutURLs")),
supported_identity_providers: parse_cognito_string_array(
props.get("SupportedIdentityProviders"),
),
allowed_o_auth_flows: parse_cognito_string_array(props.get("AllowedOAuthFlows")),
allowed_o_auth_scopes: parse_cognito_string_array(props.get("AllowedOAuthScopes")),
allowed_o_auth_flows_user_pool_client: props
.get("AllowedOAuthFlowsUserPoolClient")
.and_then(|v| v.as_bool())
.unwrap_or(false),
prevent_user_existence_errors: props
.get("PreventUserExistenceErrors")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
read_attributes: parse_cognito_string_array(props.get("ReadAttributes")),
write_attributes: parse_cognito_string_array(props.get("WriteAttributes")),
creation_date: now,
last_modified_date: now,
enable_token_revocation: props
.get("EnableTokenRevocation")
.and_then(|v| v.as_bool())
.unwrap_or(true),
auth_session_validity: props.get("AuthSessionValidity").and_then(|v| v.as_i64()),
client_secrets: Vec::new(),
refresh_token_rotation: None,
};
state.user_pool_clients.insert(client_id.clone(), client);
let mut result = ProvisionResult::new(client_id.clone())
.with("ClientId", client_id.clone())
.with("Name", client_id);
if let Some(secret) = client_secret {
result = result.with("ClientSecret", secret);
}
Ok(result)
}
fn delete_cognito_user_pool_client(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cognito_state.write();
let state = accounts.get_or_create(&self.account_id);
state.user_pool_clients.remove(physical_id);
Ok(())
}
fn create_cognito_user_pool_domain(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let domain = props
.get("Domain")
.and_then(|v| v.as_str())
.ok_or_else(|| "Domain is required".to_string())?
.to_string();
let pool_id = props
.get("UserPoolId")
.and_then(|v| v.as_str())
.ok_or_else(|| "UserPoolId is required".to_string())?
.to_string();
let custom_domain_config = props
.get("CustomDomainConfig")
.and_then(|v| v.as_object())
.and_then(|m| {
m.get("CertificateArn")
.and_then(|v| v.as_str())
.map(|s| CustomDomainConfig {
certificate_arn: s.to_string(),
})
});
let mut accounts = self.cognito_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.user_pools.contains_key(&pool_id) {
return Err(format!(
"User pool {pool_id} does not exist yet — retry once it has been provisioned"
));
}
if state.domains.contains_key(&domain) {
return Err(format!("Domain {domain} already exists"));
}
state.domains.insert(
domain.clone(),
UserPoolDomain {
user_pool_id: pool_id,
domain: domain.clone(),
status: "ACTIVE".to_string(),
custom_domain_config: custom_domain_config.clone(),
creation_date: Utc::now(),
},
);
let cloudfront_distribution = if custom_domain_config.is_some() {
format!("{domain}.cloudfront.net")
} else {
format!("{domain}.auth.{}.amazoncognito.com", self.region)
};
Ok(ProvisionResult::new(domain.clone())
.with("Domain", domain)
.with("CloudFrontDistribution", cloudfront_distribution))
}
fn delete_cognito_user_pool_domain(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cognito_state.write();
let state = accounts.get_or_create(&self.account_id);
state.domains.remove(physical_id);
Ok(())
}
fn create_cognito_identity_pool(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let identity_pool_name = props
.get("IdentityPoolName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let allow_unauth = props
.get("AllowUnauthenticatedIdentities")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let allow_classic = props
.get("AllowClassicFlow")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let developer_provider_name = props
.get("DeveloperProviderName")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let cognito_identity_providers = props
.get("CognitoIdentityProviders")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|p| {
let obj = p.as_object()?;
let provider_name = obj
.get("ProviderName")
.and_then(|v| v.as_str())?
.to_string();
let client_id = obj.get("ClientId").and_then(|v| v.as_str())?.to_string();
let server_side_token_check = obj
.get("ServerSideTokenCheck")
.and_then(|v| v.as_bool())
.unwrap_or(false);
Some(CognitoIdentityProvider {
provider_name,
client_id,
server_side_token_check,
})
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
let open_id_connect_provider_arns =
parse_cognito_string_array(props.get("OpenIdConnectProviderARNs"));
let saml_provider_arns = parse_cognito_string_array(props.get("SamlProviderARNs"));
let supported_login_providers = props
.get("SupportedLoginProviders")
.and_then(|v| v.as_object())
.map(|m| {
m.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let identity_pool_tags = parse_cognito_tags(props.get("IdentityPoolTags"));
let cognito_streams = props.get("CognitoStreams").cloned();
let push_sync = props.get("PushSync").cloned();
let identity_pool_id = format!("{}:{}", self.region, Uuid::new_v4());
let pool = IdentityPool {
identity_pool_id: identity_pool_id.clone(),
identity_pool_name,
allow_unauthenticated_identities: allow_unauth,
allow_classic_flow: allow_classic,
developer_provider_name,
cognito_identity_providers,
open_id_connect_provider_arns,
saml_provider_arns,
supported_login_providers,
cognito_streams,
push_sync,
identity_pool_tags,
creation_date: Utc::now(),
};
let mut accounts = self.cognito_state.write();
let state = accounts.get_or_create(&self.account_id);
state.identity_pools.insert(identity_pool_id.clone(), pool);
Ok(ProvisionResult::new(identity_pool_id.clone()).with("Name", identity_pool_id))
}
fn delete_cognito_identity_pool(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cognito_state.write();
let state = accounts.get_or_create(&self.account_id);
state.identity_pools.remove(physical_id);
state
.identity_pool_role_attachments
.retain(|_, a| a.identity_pool_id != physical_id);
Ok(())
}
fn create_cognito_identity_pool_role_attachment(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let identity_pool_id = props
.get("IdentityPoolId")
.and_then(|v| v.as_str())
.ok_or_else(|| "IdentityPoolId is required".to_string())?
.to_string();
let mut accounts = self.cognito_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.identity_pools.contains_key(&identity_pool_id) {
return Err(format!(
"Identity pool {identity_pool_id} does not exist yet — retry once it has been provisioned"
));
}
let roles = props
.get("Roles")
.and_then(|v| v.as_object())
.map(|m| {
m.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let role_mappings = props
.get("RoleMappings")
.and_then(|v| v.as_object())
.map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default();
let attachment_id = Uuid::new_v4().simple().to_string();
let physical_id = format!("{identity_pool_id}:{attachment_id}");
let attachment = IdentityPoolRoleAttachment {
identity_pool_id: identity_pool_id.clone(),
attachment_id,
roles,
role_mappings,
};
state
.identity_pool_role_attachments
.insert(physical_id.clone(), attachment);
Ok(ProvisionResult::new(physical_id))
}
fn delete_cognito_identity_pool_role_attachment(
&self,
physical_id: &str,
) -> Result<(), String> {
let mut accounts = self.cognito_state.write();
let state = accounts.get_or_create(&self.account_id);
state.identity_pool_role_attachments.remove(physical_id);
Ok(())
}
fn create_rds_subnet_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("DBSubnetGroupName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let description = props
.get("DBSubnetGroupDescription")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let subnet_ids: Vec<String> = props
.get("SubnetIds")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let tags = parse_rds_tags(props.get("Tags"));
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
let arn = state.db_subnet_group_arn(&name);
let group = DbSubnetGroup {
db_subnet_group_name: name.clone(),
db_subnet_group_arn: arn.clone(),
db_subnet_group_description: description,
vpc_id: String::new(),
subnet_ids,
subnet_availability_zones: Vec::new(),
tags,
};
state.subnet_groups.insert(name.clone(), group);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_rds_subnet_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
state.subnet_groups.remove(physical_id);
Ok(())
}
fn create_rds_parameter_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("DBParameterGroupName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let family = props
.get("Family")
.and_then(|v| v.as_str())
.unwrap_or("postgres16")
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let parameters: std::collections::BTreeMap<String, String> = props
.get("Parameters")
.and_then(|v| v.as_object())
.map(|m| {
m.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let tags = parse_rds_tags(props.get("Tags"));
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
let arn = state.db_parameter_group_arn(&name);
let group = DbParameterGroup {
db_parameter_group_name: name.clone(),
db_parameter_group_arn: arn.clone(),
db_parameter_group_family: family,
description,
parameters,
tags,
};
state.parameter_groups.insert(name.clone(), group);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_rds_parameter_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
state.parameter_groups.remove(physical_id);
Ok(())
}
fn create_rds_cluster_parameter_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("DBClusterParameterGroupName")
.or_else(|| props.get("Name"))
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let family = props
.get("Family")
.and_then(|v| v.as_str())
.unwrap_or("aurora-postgresql15")
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let arn = format!(
"arn:aws:rds:{}:{}:cluster-pg:{}",
self.region, self.account_id, name
);
let entry = serde_json::json!({
"DBClusterParameterGroupName": name,
"DBClusterParameterGroupArn": arn,
"DBParameterGroupFamily": family,
"Description": description,
});
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
rds_extras_mut(state, "cluster_param_groups").insert(name.clone(), entry);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_rds_cluster_parameter_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(m) = state.extras.get_mut("cluster_param_groups") {
m.remove(physical_id);
}
Ok(())
}
fn create_rds_option_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("OptionGroupName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let engine_name = props
.get("EngineName")
.and_then(|v| v.as_str())
.unwrap_or("mysql")
.to_string();
let major_engine_version = props
.get("MajorEngineVersion")
.and_then(|v| v.as_str())
.unwrap_or("8.0")
.to_string();
let description = props
.get("OptionGroupDescription")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let arn = format!(
"arn:aws:rds:{}:{}:og:{}",
self.region, self.account_id, name
);
let entry = serde_json::json!({
"OptionGroupName": name,
"OptionGroupArn": arn,
"EngineName": engine_name,
"MajorEngineVersion": major_engine_version,
"OptionGroupDescription": description,
});
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
rds_extras_mut(state, "option_groups").insert(name.clone(), entry);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_rds_option_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(m) = state.extras.get_mut("option_groups") {
m.remove(physical_id);
}
Ok(())
}
fn create_rds_event_subscription(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("SubscriptionName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let sns_topic_arn = props
.get("SnsTopicArn")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let entry = serde_json::json!({
"CustSubscriptionId": name,
"SnsTopicArn": sns_topic_arn,
"Status": "active",
"Enabled": props.get("Enabled").and_then(|v| v.as_bool()).unwrap_or(true),
});
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
rds_extras_mut(state, "event_subscriptions").insert(name.clone(), entry);
Ok(ProvisionResult::new(name))
}
fn delete_rds_event_subscription(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(m) = state.extras.get_mut("event_subscriptions") {
m.remove(physical_id);
}
Ok(())
}
fn create_rds_security_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("DBSecurityGroupName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let description = props
.get("GroupDescription")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let entry = serde_json::json!({
"DBSecurityGroupName": name,
"DBSecurityGroupDescription": description,
"OwnerId": self.account_id,
});
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
rds_extras_mut(state, "security_groups").insert(name.clone(), entry);
Ok(ProvisionResult::new(name))
}
fn delete_rds_security_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(m) = state.extras.get_mut("security_groups") {
m.remove(physical_id);
}
Ok(())
}
fn create_rds_db_proxy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("DBProxyName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let engine_family = props
.get("EngineFamily")
.and_then(|v| v.as_str())
.unwrap_or("POSTGRESQL")
.to_string();
let arn = format!(
"arn:aws:rds:{}:{}:db-proxy:{}",
self.region, self.account_id, name
);
let endpoint = format!("{name}.proxy-default.{}.rds.amazonaws.com", self.region);
let entry = serde_json::json!({
"DBProxyName": name,
"DBProxyArn": arn,
"Status": "available",
"EngineFamily": engine_family,
"Endpoint": endpoint,
});
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
rds_extras_mut(state, "proxies").insert(name.clone(), entry);
Ok(ProvisionResult::new(name)
.with("DBProxyArn", arn)
.with("Endpoint", endpoint))
}
fn delete_rds_db_proxy(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(m) = state.extras.get_mut("proxies") {
m.remove(physical_id);
}
Ok(())
}
fn create_rds_db_instance(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let identifier = props
.get("DBInstanceIdentifier")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| {
format!(
"cfn-{}-{}",
resource.logical_id.to_lowercase(),
Uuid::new_v4().simple().to_string()[..8].to_lowercase()
)
});
let class = props
.get("DBInstanceClass")
.and_then(|v| v.as_str())
.unwrap_or("db.t4g.micro")
.to_string();
let engine = props
.get("Engine")
.and_then(|v| v.as_str())
.unwrap_or("postgres")
.to_string();
let engine_version = props
.get("EngineVersion")
.and_then(|v| v.as_str())
.unwrap_or("16.0")
.to_string();
let master_username = props
.get("MasterUsername")
.and_then(|v| v.as_str())
.unwrap_or("admin")
.to_string();
let master_user_password = props
.get("MasterUserPassword")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let db_name = props
.get("DBName")
.and_then(|v| v.as_str())
.map(String::from);
let port = props
.get("Port")
.and_then(|v| v.as_i64())
.map(|n| n as i32)
.unwrap_or(5432);
let allocated_storage = props
.get("AllocatedStorage")
.and_then(|v| {
v.as_i64()
.or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
})
.map(|n| n as i32)
.unwrap_or(20);
let publicly_accessible = props
.get("PubliclyAccessible")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let deletion_protection = props
.get("DeletionProtection")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let backup_retention_period = props
.get("BackupRetentionPeriod")
.and_then(|v| v.as_i64())
.map(|n| n as i32)
.unwrap_or(0);
let multi_az = props
.get("MultiAZ")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let availability_zone = props
.get("AvailabilityZone")
.and_then(|v| v.as_str())
.map(String::from);
let storage_type = props
.get("StorageType")
.and_then(|v| v.as_str())
.map(String::from);
let storage_encrypted = props
.get("StorageEncrypted")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let kms_key_id = props
.get("KmsKeyId")
.and_then(|v| v.as_str())
.map(String::from);
let iam_database_authentication_enabled = props
.get("EnableIAMDatabaseAuthentication")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let db_parameter_group_name = props
.get("DBParameterGroupName")
.and_then(|v| v.as_str())
.map(String::from);
let option_group_name = props
.get("OptionGroupName")
.and_then(|v| v.as_str())
.map(String::from);
let vpc_security_group_ids: Vec<String> = props
.get("VPCSecurityGroups")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let enabled_cloudwatch_logs_exports: Vec<String> = props
.get("EnableCloudwatchLogsExports")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let tags = parse_rds_tags(props.get("Tags"));
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
let arn = state.db_instance_arn(&identifier);
let endpoint_address = format!(
"{identifier}.cluster-fakecloud.{}.rds.amazonaws.com",
state.region
);
let dbi_resource_id = format!("db-{}", Uuid::new_v4().simple());
let inst = DbInstance {
db_instance_identifier: identifier.clone(),
db_instance_arn: arn.clone(),
db_instance_class: class,
engine,
engine_version,
db_instance_status: "available".to_string(),
master_username,
db_name,
endpoint_address,
port,
allocated_storage,
publicly_accessible,
deletion_protection,
created_at: Utc::now(),
dbi_resource_id,
master_user_password,
container_id: String::new(),
host_port: 0,
tags,
read_replica_source_db_instance_identifier: None,
read_replica_db_instance_identifiers: Vec::new(),
vpc_security_group_ids,
db_parameter_group_name,
backup_retention_period,
preferred_backup_window: "03:00-04:00".to_string(),
preferred_maintenance_window: None,
latest_restorable_time: None,
option_group_name,
multi_az,
pending_modified_values: None,
availability_zone,
storage_type,
storage_encrypted,
kms_key_id,
iam_database_authentication_enabled,
iops: props.get("Iops").and_then(|v| v.as_i64()).map(|n| n as i32),
monitoring_interval: props
.get("MonitoringInterval")
.and_then(|v| v.as_i64())
.map(|n| n as i32),
monitoring_role_arn: props
.get("MonitoringRoleArn")
.and_then(|v| v.as_str())
.map(String::from),
performance_insights_enabled: props
.get("EnablePerformanceInsights")
.and_then(|v| v.as_bool())
.unwrap_or(false),
performance_insights_kms_key_id: props
.get("PerformanceInsightsKMSKeyId")
.and_then(|v| v.as_str())
.map(String::from),
performance_insights_retention_period: props
.get("PerformanceInsightsRetentionPeriod")
.and_then(|v| v.as_i64())
.map(|n| n as i32),
enabled_cloudwatch_logs_exports,
ca_certificate_identifier: props
.get("CACertificateIdentifier")
.and_then(|v| v.as_str())
.map(String::from),
network_type: props
.get("NetworkType")
.and_then(|v| v.as_str())
.map(String::from),
character_set_name: props
.get("CharacterSetName")
.and_then(|v| v.as_str())
.map(String::from),
auto_minor_version_upgrade: props
.get("AutoMinorVersionUpgrade")
.and_then(|v| v.as_bool()),
copy_tags_to_snapshot: props.get("CopyTagsToSnapshot").and_then(|v| v.as_bool()),
master_user_secret_arn: None,
master_user_secret_kms_key_id: props
.get("MasterUserSecret")
.and_then(|v| v.get("KmsKeyId"))
.and_then(|v| v.as_str())
.map(String::from),
license_model: props
.get("LicenseModel")
.and_then(|v| v.as_str())
.map(String::from),
max_allocated_storage: props
.get("MaxAllocatedStorage")
.and_then(|v| v.as_i64())
.map(|n| n as i32),
multi_tenant: props.get("MultiTenant").and_then(|v| v.as_bool()),
storage_throughput: props
.get("StorageThroughput")
.and_then(|v| v.as_i64())
.map(|n| n as i32),
tde_credential_arn: props
.get("TdeCredentialArn")
.and_then(|v| v.as_str())
.map(String::from),
delete_automated_backups: props
.get("DeleteAutomatedBackups")
.and_then(|v| v.as_bool()),
db_security_groups: props
.get("DBSecurityGroups")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default(),
domain: props
.get("Domain")
.and_then(|v| v.as_str())
.map(String::from),
domain_fqdn: props
.get("DomainFqdn")
.and_then(|v| v.as_str())
.map(String::from),
domain_ou: props
.get("DomainOu")
.and_then(|v| v.as_str())
.map(String::from),
domain_iam_role_name: props
.get("DomainIAMRoleName")
.and_then(|v| v.as_str())
.map(String::from),
domain_auth_secret_arn: props
.get("DomainAuthSecretArn")
.and_then(|v| v.as_str())
.map(String::from),
domain_dns_ips: props
.get("DomainDnsIps")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default(),
db_cluster_identifier: props
.get("DBClusterIdentifier")
.and_then(|v| v.as_str())
.map(String::from),
};
let endpoint = inst.endpoint_address.clone();
let endpoint_port = inst.port;
state.instances.insert(identifier.clone(), inst);
Ok(ProvisionResult::new(identifier.clone())
.with("DBInstanceArn", arn)
.with("Endpoint.Address", endpoint)
.with("Endpoint.Port", endpoint_port.to_string())
.with("DbiResourceId", format!("db-{identifier}")))
}
fn delete_rds_db_instance(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
state.instances.remove(physical_id);
Ok(())
}
fn create_rds_db_cluster(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let identifier = props
.get("DBClusterIdentifier")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| {
format!(
"cfn-cluster-{}-{}",
resource.logical_id.to_lowercase(),
Uuid::new_v4().simple().to_string()[..8].to_lowercase()
)
});
let engine = props
.get("Engine")
.and_then(|v| v.as_str())
.unwrap_or("aurora-postgresql")
.to_string();
let engine_version = props
.get("EngineVersion")
.and_then(|v| v.as_str())
.map(String::from);
let master_username = props
.get("MasterUsername")
.and_then(|v| v.as_str())
.map(String::from);
let port = props.get("Port").and_then(|v| v.as_i64()).unwrap_or(5432);
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
let arn = format!(
"arn:aws:rds:{}:{}:cluster:{}",
state.region, state.account_id, identifier
);
let cluster_resource_id = format!("cluster-{}", Uuid::new_v4().simple());
let endpoint = format!(
"{identifier}.cluster-fakecloud.{}.rds.amazonaws.com",
state.region
);
let reader_endpoint = format!(
"{identifier}.cluster-ro-fakecloud.{}.rds.amazonaws.com",
state.region
);
let body = serde_json::json!({
"DBClusterIdentifier": identifier,
"DBClusterArn": arn,
"Engine": engine,
"EngineVersion": engine_version,
"MasterUsername": master_username,
"Status": "available",
"DbClusterResourceId": cluster_resource_id,
"Endpoint": endpoint,
"ReaderEndpoint": reader_endpoint,
"Port": port,
"AllocatedStorage": props.get("AllocatedStorage").and_then(|v| v.as_i64()).unwrap_or(1),
"BackupRetentionPeriod": props.get("BackupRetentionPeriod").and_then(|v| v.as_i64()).unwrap_or(1),
"DatabaseName": props.get("DatabaseName").and_then(|v| v.as_str()),
"DBSubnetGroup": props.get("DBSubnetGroupName").and_then(|v| v.as_str()),
"VpcSecurityGroupIds": props.get("VpcSecurityGroupIds").cloned().unwrap_or(serde_json::json!([])),
"StorageEncrypted": props.get("StorageEncrypted").and_then(|v| v.as_bool()).unwrap_or(false),
"KmsKeyId": props.get("KmsKeyId").and_then(|v| v.as_str()),
"DeletionProtection": props.get("DeletionProtection").and_then(|v| v.as_bool()).unwrap_or(false),
"ClusterCreateTime": Utc::now().to_rfc3339(),
"EnabledCloudwatchLogsExports": props.get("EnableCloudwatchLogsExports").cloned().unwrap_or(serde_json::json!([])),
"MultiAZ": false,
"DBClusterMembers": [],
});
state
.extras
.entry("clusters".to_string())
.or_default()
.insert(identifier.clone(), body);
Ok(ProvisionResult::new(identifier.clone())
.with("DBClusterArn", arn)
.with("Endpoint.Address", endpoint)
.with("ReadEndpoint.Address", reader_endpoint)
.with("Endpoint.Port", port.to_string())
.with("DBClusterResourceId", cluster_resource_id))
}
fn delete_rds_db_cluster(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.rds_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(m) = state.extras.get_mut("clusters") {
m.remove(physical_id);
}
Ok(())
}
fn create_ecs_cluster(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let cluster_name = props
.get("ClusterName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let cluster_arn = format!(
"arn:aws:ecs:{}:{}:cluster/{}",
self.region, self.account_id, cluster_name
);
let mut cluster = EcsCluster::new(&cluster_name, cluster_arn.clone());
cluster.tags = parse_ecs_tags(props.get("Tags"));
cluster.capacity_providers = props
.get("CapacityProviders")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
if let Some(strategy) = props
.get("DefaultCapacityProviderStrategy")
.and_then(|v| v.as_array())
{
cluster.default_capacity_provider_strategy =
strategy.iter().cloned().map(lowercase_first_keys).collect();
}
if let Some(cfg) = props.get("Configuration") {
cluster.configuration = Some(lowercase_first_keys(cfg.clone()));
}
if let Some(settings) = props.get("ClusterSettings").and_then(|v| v.as_array()) {
cluster.settings = settings.iter().cloned().map(lowercase_first_keys).collect();
}
if let Some(scd) = props.get("ServiceConnectDefaults") {
cluster.service_connect_defaults = Some(lowercase_first_keys(scd.clone()));
}
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
state.clusters.insert(cluster_name.clone(), cluster);
Ok(ProvisionResult::new(cluster_name).with("Arn", cluster_arn))
}
fn delete_ecs_cluster(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
state.clusters.remove(physical_id);
state.services.retain(|_, s| s.cluster_name != physical_id);
state
.tasks
.retain(|_, t| t.cluster_arn.split('/').next_back() != Some(physical_id));
Ok(())
}
fn create_ecs_task_definition(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let family = props
.get("Family")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let container_definitions: Vec<serde_json::Value> = props
.get("ContainerDefinitions")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default()
.into_iter()
.map(lowercase_first_keys)
.collect();
let task_role_arn = props
.get("TaskRoleArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let execution_role_arn = props
.get("ExecutionRoleArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let network_mode = props
.get("NetworkMode")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let requires_compatibilities: Vec<String> = props
.get("RequiresCompatibilities")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let cpu = props
.get("Cpu")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let memory = props
.get("Memory")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let volumes: Vec<serde_json::Value> = props
.get("Volumes")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default()
.into_iter()
.map(lowercase_first_keys)
.collect();
let placement_constraints: Vec<serde_json::Value> = props
.get("PlacementConstraints")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default()
.into_iter()
.map(lowercase_first_keys)
.collect();
let proxy_configuration = props
.get("ProxyConfiguration")
.cloned()
.map(lowercase_first_keys);
let ephemeral_storage = props
.get("EphemeralStorage")
.cloned()
.map(lowercase_first_keys);
let runtime_platform = props
.get("RuntimePlatform")
.cloned()
.map(lowercase_first_keys);
let tags = parse_ecs_tags(props.get("Tags"));
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
let revision = state
.next_revision
.entry(family.clone())
.and_modify(|n| *n += 1)
.or_insert(1);
let revision = *revision;
let arn = format!(
"arn:aws:ecs:{}:{}:task-definition/{}:{}",
self.region, self.account_id, family, revision
);
let td = EcsTaskDefinition {
family: family.clone(),
revision,
task_definition_arn: arn.clone(),
container_definitions,
status: "ACTIVE".to_string(),
task_role_arn,
execution_role_arn,
network_mode,
requires_compatibilities: requires_compatibilities.clone(),
compatibilities: requires_compatibilities,
cpu,
memory,
pid_mode: None,
ipc_mode: None,
volumes,
placement_constraints,
proxy_configuration,
inference_accelerators: Vec::new(),
ephemeral_storage,
runtime_platform,
requires_attributes: Vec::new(),
registered_at: Utc::now(),
registered_by: None,
deregistered_at: None,
tags,
enable_fault_injection: props.get("EnableFaultInjection").and_then(|v| v.as_bool()),
};
state
.task_definitions
.entry(family.clone())
.or_default()
.insert(revision, td);
Ok(ProvisionResult::new(arn.clone()).with("TaskDefinitionArn", arn))
}
fn delete_ecs_task_definition(&self, physical_id: &str) -> Result<(), String> {
let Some(suffix) = physical_id.rsplit('/').next() else {
return Ok(());
};
let Some((family, rev)) = suffix.split_once(':') else {
return Ok(());
};
let Ok(revision) = rev.parse::<i32>() else {
return Ok(());
};
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(revs) = state.task_definitions.get_mut(family) {
if let Some(td) = revs.get_mut(&revision) {
td.status = "INACTIVE".to_string();
td.deregistered_at = Some(Utc::now());
}
}
Ok(())
}
fn create_ecs_service(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let service_name = props
.get("ServiceName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let cluster_name = props
.get("Cluster")
.and_then(|v| v.as_str())
.map(parse_ecs_cluster_name)
.unwrap_or_else(|| "default".to_string());
let task_definition_arn = props
.get("TaskDefinition")
.and_then(|v| v.as_str())
.ok_or_else(|| "TaskDefinition is required".to_string())?
.to_string();
let desired_count = props
.get("DesiredCount")
.and_then(cfn_as_i64)
.map(|n| n as i32)
.unwrap_or(1);
let launch_type = props
.get("LaunchType")
.and_then(|v| v.as_str())
.unwrap_or("FARGATE")
.to_string();
let scheduling_strategy = props
.get("SchedulingStrategy")
.and_then(|v| v.as_str())
.unwrap_or("REPLICA")
.to_string();
let deployment_controller = props
.get("DeploymentController")
.and_then(|v| v.get("Type"))
.and_then(|v| v.as_str())
.unwrap_or("ECS")
.to_string();
let load_balancers: Vec<serde_json::Value> = props
.get("LoadBalancers")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default()
.into_iter()
.map(lowercase_first_keys)
.collect();
let service_registries: Vec<serde_json::Value> = props
.get("ServiceRegistries")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default()
.into_iter()
.map(lowercase_first_keys)
.collect();
let placement_constraints: Vec<serde_json::Value> = props
.get("PlacementConstraints")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default()
.into_iter()
.map(lowercase_first_keys)
.collect();
let placement_strategy: Vec<serde_json::Value> = props
.get("PlacementStrategies")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default()
.into_iter()
.map(lowercase_first_keys)
.collect();
let network_configuration = props
.get("NetworkConfiguration")
.cloned()
.map(lowercase_first_keys);
let role_arn = props
.get("Role")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let platform_version = props
.get("PlatformVersion")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let health_check_grace_period_seconds = props
.get("HealthCheckGracePeriodSeconds")
.and_then(cfn_as_i64)
.map(|n| n as i32);
let enable_ecs_managed_tags = props
.get("EnableECSManagedTags")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let enable_execute_command = props
.get("EnableExecuteCommand")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let propagate_tags = props
.get("PropagateTags")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let capacity_provider_strategy: Vec<serde_json::Value> = props
.get("CapacityProviderStrategy")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default()
.into_iter()
.map(lowercase_first_keys)
.collect();
let availability_zone_rebalancing = props
.get("AvailabilityZoneRebalancing")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let tags = parse_ecs_tags(props.get("Tags"));
let (family, revision) = parse_td_arn(&task_definition_arn);
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.clusters.contains_key(&cluster_name) {
return Err(format!(
"Cluster {cluster_name} does not exist yet — retry once it has been provisioned"
));
}
let cluster_arn = state.clusters[&cluster_name].cluster_arn.clone();
let service_arn = format!(
"arn:aws:ecs:{}:{}:service/{}/{}",
self.region, self.account_id, cluster_name, service_name
);
let key = format!("{cluster_name}/{service_name}");
let service = EcsService {
service_name: service_name.clone(),
service_arn: service_arn.clone(),
cluster_name: cluster_name.clone(),
cluster_arn,
task_definition_arn,
family,
revision,
desired_count,
running_count: 0,
pending_count: 0,
launch_type,
status: "ACTIVE".to_string(),
scheduling_strategy,
deployment_controller,
minimum_healthy_percent: props
.get("DeploymentConfiguration")
.and_then(|v| v.get("MinimumHealthyPercent"))
.and_then(cfn_as_i64)
.map(|n| n as i32),
maximum_percent: props
.get("DeploymentConfiguration")
.and_then(|v| v.get("MaximumPercent"))
.and_then(cfn_as_i64)
.map(|n| n as i32),
circuit_breaker: None,
deployments: Vec::new(),
load_balancers,
service_registries,
placement_constraints,
placement_strategy,
network_configuration,
tags,
created_at: Utc::now(),
created_by: None,
role_arn,
platform_version,
health_check_grace_period_seconds,
enable_execute_command,
enable_ecs_managed_tags,
propagate_tags,
capacity_provider_strategy,
availability_zone_rebalancing,
volume_configurations: Vec::new(),
};
state.services.insert(key.clone(), service);
if let Some(c) = state.clusters.get_mut(&cluster_name) {
c.active_services_count += 1;
}
Ok(ProvisionResult::new(service_arn.clone())
.with("Name", service_name)
.with("ServiceArn", service_arn))
}
fn delete_ecs_service(&self, physical_id: &str) -> Result<(), String> {
let Some((cluster, service)) = parse_service_arn(physical_id) else {
return Ok(());
};
let key = format!("{cluster}/{service}");
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
if state.services.remove(&key).is_some() {
if let Some(c) = state.clusters.get_mut(&cluster) {
if c.active_services_count > 0 {
c.active_services_count -= 1;
}
}
}
Ok(())
}
fn create_ecs_capacity_provider(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let arn = format!(
"arn:aws:ecs:{}:{}:capacity-provider/{}",
self.region, self.account_id, name
);
let cp = EcsCapacityProvider {
name: name.clone(),
arn: arn.clone(),
status: "ACTIVE".to_string(),
auto_scaling_group_provider: props.get("AutoScalingGroupProvider").cloned(),
update_status: None,
update_status_reason: None,
created_at: Utc::now(),
tags: parse_ecs_tags(props.get("Tags")),
};
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
state.capacity_providers.insert(name.clone(), cp);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_ecs_capacity_provider(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
state.capacity_providers.remove(physical_id);
Ok(())
}
fn update_ecs_cluster(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let cluster_name = existing.physical_id.clone();
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
let cluster = state
.clusters
.get_mut(&cluster_name)
.ok_or_else(|| format!("Cluster {cluster_name} no longer exists"))?;
if let Some(settings) = props.get("ClusterSettings").and_then(|v| v.as_array()) {
cluster.settings = settings.iter().cloned().map(lowercase_first_keys).collect();
}
if let Some(cfg) = props.get("Configuration") {
cluster.configuration = Some(lowercase_first_keys(cfg.clone()));
}
if let Some(cps) = props.get("CapacityProviders").and_then(|v| v.as_array()) {
cluster.capacity_providers = cps
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
}
if let Some(strategy) = props
.get("DefaultCapacityProviderStrategy")
.and_then(|v| v.as_array())
{
cluster.default_capacity_provider_strategy =
strategy.iter().cloned().map(lowercase_first_keys).collect();
}
if let Some(scd) = props.get("ServiceConnectDefaults") {
cluster.service_connect_defaults = Some(lowercase_first_keys(scd.clone()));
}
if props.get("Tags").is_some() {
cluster.tags = parse_ecs_tags(props.get("Tags"));
}
let cluster_arn = cluster.cluster_arn.clone();
Ok(ProvisionResult::new(cluster_name).with("Arn", cluster_arn))
}
fn update_ecs_service(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let service_arn = existing.physical_id.clone();
let Some((cluster_name, service_name)) = parse_service_arn(&service_arn) else {
return Err(format!("Cannot parse service ARN: {service_arn}"));
};
let key = format!("{cluster_name}/{service_name}");
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
let svc = state
.services
.get_mut(&key)
.ok_or_else(|| format!("Service {service_arn} no longer exists"))?;
if let Some(td) = props.get("TaskDefinition").and_then(|v| v.as_str()) {
svc.task_definition_arn = td.to_string();
let (family, revision) = parse_td_arn(td);
svc.family = family;
svc.revision = revision;
}
if let Some(n) = props.get("DesiredCount").and_then(cfn_as_i64) {
svc.desired_count = n as i32;
}
if let Some(s) = props.get("LaunchType").and_then(|v| v.as_str()) {
svc.launch_type = s.to_string();
}
if let Some(s) = props.get("PlatformVersion").and_then(|v| v.as_str()) {
svc.platform_version = Some(s.to_string());
}
if let Some(n) = props
.get("HealthCheckGracePeriodSeconds")
.and_then(cfn_as_i64)
{
svc.health_check_grace_period_seconds = Some(n as i32);
}
if let Some(b) = props.get("EnableExecuteCommand").and_then(|v| v.as_bool()) {
svc.enable_execute_command = b;
}
if let Some(b) = props.get("EnableECSManagedTags").and_then(|v| v.as_bool()) {
svc.enable_ecs_managed_tags = b;
}
if let Some(s) = props.get("PropagateTags").and_then(|v| v.as_str()) {
svc.propagate_tags = Some(s.to_string());
}
if let Some(s) = props
.get("AvailabilityZoneRebalancing")
.and_then(|v| v.as_str())
{
svc.availability_zone_rebalancing = Some(s.to_string());
}
if let Some(arr) = props
.get("CapacityProviderStrategy")
.and_then(|v| v.as_array())
{
svc.capacity_provider_strategy =
arr.iter().cloned().map(lowercase_first_keys).collect();
}
if let Some(dc) = props.get("DeploymentConfiguration") {
if let Some(n) = dc.get("MinimumHealthyPercent").and_then(cfn_as_i64) {
svc.minimum_healthy_percent = Some(n as i32);
}
if let Some(n) = dc.get("MaximumPercent").and_then(cfn_as_i64) {
svc.maximum_percent = Some(n as i32);
}
}
if let Some(arr) = props.get("LoadBalancers").and_then(|v| v.as_array()) {
svc.load_balancers = arr.iter().cloned().map(lowercase_first_keys).collect();
}
if let Some(arr) = props.get("ServiceRegistries").and_then(|v| v.as_array()) {
svc.service_registries = arr.iter().cloned().map(lowercase_first_keys).collect();
}
if let Some(arr) = props.get("PlacementConstraints").and_then(|v| v.as_array()) {
svc.placement_constraints = arr.iter().cloned().map(lowercase_first_keys).collect();
}
if let Some(arr) = props.get("PlacementStrategies").and_then(|v| v.as_array()) {
svc.placement_strategy = arr.iter().cloned().map(lowercase_first_keys).collect();
}
if let Some(nc) = props.get("NetworkConfiguration") {
svc.network_configuration = Some(lowercase_first_keys(nc.clone()));
}
if props.get("Tags").is_some() {
svc.tags = parse_ecs_tags(props.get("Tags"));
}
let name = svc.service_name.clone();
Ok(ProvisionResult::new(service_arn.clone())
.with("Name", name)
.with("ServiceArn", service_arn))
}
fn update_ecs_task_definition(
&self,
_existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
self.create_ecs_task_definition(resource)
}
fn update_ecs_capacity_provider(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = existing.physical_id.clone();
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
let cp = state
.capacity_providers
.get_mut(&name)
.ok_or_else(|| format!("CapacityProvider {name} no longer exists"))?;
if props.get("AutoScalingGroupProvider").is_some() {
cp.auto_scaling_group_provider = props.get("AutoScalingGroupProvider").cloned();
}
if props.get("Tags").is_some() {
cp.tags = parse_ecs_tags(props.get("Tags"));
}
let arn = cp.arn.clone();
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn get_att_ecs_cluster(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
let cluster = state.clusters.get(physical_id)?;
match attribute {
"Arn" => Some(cluster.cluster_arn.clone()),
_ => None,
}
}
fn get_att_ecs_service(&self, physical_id: &str, attribute: &str) -> Option<String> {
let (cluster, service) = parse_service_arn(physical_id)?;
let key = format!("{cluster}/{service}");
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
let svc = state.services.get(&key)?;
match attribute {
"Name" => Some(svc.service_name.clone()),
"ServiceArn" => Some(svc.service_arn.clone()),
_ => None,
}
}
fn get_att_ecs_capacity_provider(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.ecs_state.write();
let state = accounts.get_or_create(&self.account_id);
let cp = state.capacity_providers.get(physical_id)?;
match attribute {
"Arn" => Some(cp.arn.clone()),
_ => None,
}
}
fn create_acm_certificate(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let domain_name = props
.get("DomainName")
.and_then(|v| v.as_str())
.ok_or_else(|| "DomainName is required".to_string())?
.to_string();
let sans: Vec<String> = props
.get("SubjectAlternativeNames")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let key_algorithm = props
.get("KeyAlgorithm")
.and_then(|v| v.as_str())
.unwrap_or("RSA_2048")
.to_string();
let validation_method = props
.get("ValidationMethod")
.and_then(|v| v.as_str())
.unwrap_or("DNS")
.to_string();
let ca_arn = props
.get("CertificateAuthorityArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let tags = parse_acm_tags(props.get("Tags"));
let cert_transparency = props
.get("CertificateTransparencyLoggingPreference")
.and_then(|v| v.as_str())
.unwrap_or("ENABLED")
.to_string();
let arn = format!(
"arn:aws:acm:{}:{}:certificate/{}",
self.region,
self.account_id,
Uuid::new_v4()
);
let now = Utc::now();
let mut all_names = vec![domain_name.clone()];
for s in &sans {
if !all_names.contains(s) {
all_names.push(s.clone());
}
}
let (cert_pem, key_pem) = rcgen::generate_simple_self_signed(all_names.clone())
.map(|c| (c.cert.pem(), c.key_pair.serialize_pem()))
.map(|(c, k)| (Some(c), Some(k)))
.unwrap_or((None, None));
let domain_validation: Vec<AcmDomainValidation> =
synth_acm_domain_validation(&domain_name, &sans, &validation_method)
.into_iter()
.map(|mut dv| {
dv.validation_status = "SUCCESS".to_string();
dv
})
.collect();
let renewal_summary = Some(AcmRenewalSummary {
renewal_status: "PENDING_AUTO_RENEWAL".to_string(),
domain_validation: domain_validation.clone(),
renewal_status_reason: None,
updated_at: now,
});
let cert = AcmStoredCertificate {
arn: arn.clone(),
domain_name: domain_name.clone(),
subject_alternative_names: all_names,
status: "ISSUED".to_string(),
cert_type: "AMAZON_ISSUED".to_string(),
certificate_pem: cert_pem,
certificate_chain_pem: None,
private_key_pem: key_pem,
idempotency_token: None,
serial: format!("{:032x}", Uuid::new_v4().as_u128()),
subject: format!("CN={domain_name}"),
issuer: "Amazon".to_string(),
key_algorithm,
signature_algorithm: "SHA256WITHRSA".to_string(),
created_at: now,
issued_at: Some(now),
imported_at: None,
revoked_at: None,
revocation_reason: None,
not_before: now,
not_after: now + chrono::Duration::days(395),
validation_method: Some(validation_method.clone()),
domain_validation,
options: AcmCertificateOptions {
certificate_transparency_logging_preference: cert_transparency,
export: "DISABLED".to_string(),
},
renewal_eligibility: "ELIGIBLE".to_string(),
managed_by: None,
certificate_authority_arn: ca_arn,
tags,
in_use_by: Vec::new(),
describe_read_count: 0,
failure_reason: None,
renewal_summary,
};
let mut accounts = self.acm_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
account.certificates.insert(arn.clone(), cert);
Ok(ProvisionResult::new(arn))
}
fn delete_acm_certificate(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.acm_state.write();
if let Some(account) = accounts.accounts.get_mut(&self.account_id) {
account.certificates.remove(physical_id);
}
Ok(())
}
fn create_acm_account(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let days = resource
.properties
.get("ExpiryEventsConfiguration")
.and_then(|v| v.get("DaysBeforeExpiry"))
.and_then(|v| v.as_i64())
.map(|n| n as i32);
let mut accounts = self.acm_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
account.account_config.expiry_events_days_before_expiry = days;
Ok(ProvisionResult::new(format!(
"acm-account-{}",
self.account_id
)))
}
fn delete_acm_account(&self) -> Result<(), String> {
let mut accounts = self.acm_state.write();
if let Some(account) = accounts.accounts.get_mut(&self.account_id) {
account.account_config.expiry_events_days_before_expiry = None;
}
Ok(())
}
fn create_ec_parameter_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("CacheParameterGroupName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let family = props
.get("CacheParameterGroupFamily")
.and_then(|v| v.as_str())
.unwrap_or("redis7")
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let arn = format!(
"arn:aws:elasticache:{}:{}:parametergroup:{}",
self.region, self.account_id, name
);
let group = CacheParameterGroup {
cache_parameter_group_name: name.clone(),
cache_parameter_group_family: family,
description,
is_global: false,
arn: arn.clone(),
};
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
state
.parameter_groups
.retain(|p| p.cache_parameter_group_name != name);
state.parameter_groups.push(group);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_ec_parameter_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
state
.parameter_groups
.retain(|p| p.cache_parameter_group_name != physical_id);
Ok(())
}
fn create_ec_subnet_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("CacheSubnetGroupName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let subnet_ids: Vec<String> = props
.get("SubnetIds")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let arn = format!(
"arn:aws:elasticache:{}:{}:subnetgroup:{}",
self.region, self.account_id, name
);
let group = CacheSubnetGroup {
cache_subnet_group_name: name.clone(),
cache_subnet_group_description: description,
vpc_id: String::new(),
subnet_ids,
arn: arn.clone(),
};
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
state.subnet_groups.insert(name.clone(), group);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_ec_subnet_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
state.subnet_groups.remove(physical_id);
Ok(())
}
fn create_ec_security_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("CacheSecurityGroupName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let arn = format!(
"arn:aws:elasticache:{}:{}:securitygroup:{}",
self.region, self.account_id, name
);
let group = CacheSecurityGroup {
cache_security_group_name: name.clone(),
description,
owner_id: self.account_id.clone(),
arn: arn.clone(),
ec2_security_groups: Vec::new(),
};
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
state.security_groups.insert(name.clone(), group);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
fn delete_ec_security_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
state.security_groups.remove(physical_id);
Ok(())
}
fn create_ec_user(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let user_id = props
.get("UserId")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let user_name = props
.get("UserName")
.and_then(|v| v.as_str())
.unwrap_or(&user_id)
.to_string();
let engine = props
.get("Engine")
.and_then(|v| v.as_str())
.unwrap_or("redis")
.to_string();
let access_string = props
.get("AccessString")
.and_then(|v| v.as_str())
.unwrap_or("on ~* +@all")
.to_string();
let authentication_type = props
.get("AuthenticationMode")
.and_then(|v| v.get("Type"))
.and_then(|v| v.as_str())
.unwrap_or("no-password-required")
.to_string();
let arn = format!(
"arn:aws:elasticache:{}:{}:user:{}",
self.region, self.account_id, user_id
);
let user = EcUser {
user_id: user_id.clone(),
user_name,
engine,
access_string,
status: "active".to_string(),
authentication_type,
password_count: 0,
arn: arn.clone(),
minimum_engine_version: "6.0".to_string(),
user_group_ids: Vec::new(),
};
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
state.users.insert(user_id.clone(), user);
Ok(ProvisionResult::new(user_id).with("Arn", arn))
}
fn delete_ec_user(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
state.users.remove(physical_id);
Ok(())
}
fn create_ec_user_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let user_group_id = props
.get("UserGroupId")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let engine = props
.get("Engine")
.and_then(|v| v.as_str())
.unwrap_or("redis")
.to_string();
let user_ids: Vec<String> = props
.get("UserIds")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let arn = format!(
"arn:aws:elasticache:{}:{}:usergroup:{}",
self.region, self.account_id, user_group_id
);
let group = EcUserGroup {
user_group_id: user_group_id.clone(),
engine,
status: "active".to_string(),
user_ids,
arn: arn.clone(),
minimum_engine_version: "6.0".to_string(),
pending_changes: None,
replication_groups: Vec::new(),
};
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
state.user_groups.insert(user_group_id.clone(), group);
Ok(ProvisionResult::new(user_group_id).with("Arn", arn))
}
fn delete_ec_user_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
state.user_groups.remove(physical_id);
Ok(())
}
fn create_ec_cache_cluster(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let id = props
.get("ClusterName")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| format!("cfn-cc-{}", resource.logical_id.to_lowercase()));
let cache_node_type = props
.get("CacheNodeType")
.and_then(|v| v.as_str())
.unwrap_or("cache.t4g.micro")
.to_string();
let engine = props
.get("Engine")
.and_then(|v| v.as_str())
.unwrap_or("redis")
.to_string();
let engine_version = props
.get("EngineVersion")
.and_then(|v| v.as_str())
.unwrap_or("7.1")
.to_string();
let num_cache_nodes = props
.get("NumCacheNodes")
.and_then(|v| v.as_i64())
.map(|n| n as i32)
.unwrap_or(1);
let preferred_az = props
.get("PreferredAvailabilityZone")
.and_then(|v| v.as_str())
.unwrap_or("us-east-1a")
.to_string();
let cache_subnet_group_name = props
.get("CacheSubnetGroupName")
.and_then(|v| v.as_str())
.map(String::from);
let auto_minor_version_upgrade = props
.get("AutoMinorVersionUpgrade")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let default_port = if engine == "memcached" { 11211 } else { 6379 };
let port = props
.get("Port")
.and_then(|v| v.as_i64())
.map(|n| n as u16)
.unwrap_or(default_port);
let cache_parameter_group_name = props
.get("CacheParameterGroupName")
.and_then(|v| v.as_str())
.map(String::from);
let security_group_ids: Vec<String> = props
.get("VpcSecurityGroupIds")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let cache_security_group_names: Vec<String> = props
.get("CacheSecurityGroupNames")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let preferred_availability_zones: Vec<String> = props
.get("PreferredAvailabilityZones")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let snapshot_arns: Vec<String> = props
.get("SnapshotArns")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let snapshot_name = props
.get("SnapshotName")
.and_then(|v| v.as_str())
.map(String::from);
let snapshot_retention_limit = props
.get("SnapshotRetentionLimit")
.and_then(|v| v.as_i64())
.map(|n| n as i32)
.unwrap_or(0);
let snapshot_window = props
.get("SnapshotWindow")
.and_then(|v| v.as_str())
.map(String::from);
let preferred_maintenance_window = props
.get("PreferredMaintenanceWindow")
.and_then(|v| v.as_str())
.map(String::from);
let notification_topic_arn = props
.get("NotificationTopicArn")
.and_then(|v| v.as_str())
.map(String::from);
let transit_encryption_enabled = props
.get("TransitEncryptionEnabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let auth_token = props
.get("AuthToken")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(String::from);
let auth_token_enabled = auth_token.is_some();
let network_type = props
.get("NetworkType")
.and_then(|v| v.as_str())
.map(String::from)
.or_else(|| Some("ipv4".to_string()));
let ip_discovery = props
.get("IpDiscovery")
.and_then(|v| v.as_str())
.map(String::from)
.or_else(|| Some("ipv4".to_string()));
let az_mode = props
.get("AZMode")
.and_then(|v| v.as_str())
.map(String::from)
.or_else(|| Some("single-az".to_string()));
let outpost_mode = props
.get("OutpostMode")
.and_then(|v| v.as_str())
.map(String::from);
let preferred_outpost_arn = props
.get("PreferredOutpostArn")
.and_then(|v| v.as_str())
.map(String::from);
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
let arn = format!(
"arn:aws:elasticache:{}:{}:cluster:{}",
state.region, state.account_id, id
);
let endpoint_address = format!("{id}.fakecloud.{}.cache.amazonaws.com", state.region);
let cluster = EcCacheCluster {
cache_cluster_id: id.clone(),
cache_node_type,
engine,
engine_version,
cache_cluster_status: "available".to_string(),
num_cache_nodes,
preferred_availability_zone: preferred_az,
cache_subnet_group_name,
auto_minor_version_upgrade,
arn: arn.clone(),
created_at: Utc::now().to_rfc3339(),
endpoint_address: endpoint_address.clone(),
endpoint_port: port,
container_id: String::new(),
host_port: 0,
replication_group_id: None,
cache_parameter_group_name,
security_group_ids,
log_delivery_configurations: Vec::new(),
transit_encryption_enabled,
at_rest_encryption_enabled: false,
auth_token_enabled,
port,
preferred_maintenance_window,
preferred_availability_zones,
notification_topic_arn,
cache_security_group_names,
snapshot_arns,
snapshot_name,
snapshot_retention_limit,
snapshot_window,
outpost_mode,
preferred_outpost_arn,
network_type,
ip_discovery,
az_mode,
auth_token,
kms_key_id: None,
transit_encryption_mode: None,
data_tiering_enabled: None,
cluster_mode: None,
preferred_outpost_arns: Vec::new(),
};
state.cache_clusters.insert(id.clone(), cluster);
Ok(ProvisionResult::new(id.clone())
.with("Arn", arn)
.with("RedisEndpoint.Address", endpoint_address.clone())
.with("RedisEndpoint.Port", port.to_string())
.with("ConfigurationEndpoint.Address", endpoint_address)
.with("ConfigurationEndpoint.Port", port.to_string()))
}
fn delete_ec_cache_cluster(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
state.cache_clusters.remove(physical_id);
Ok(())
}
fn create_ec_replication_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let id = props
.get("ReplicationGroupId")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| format!("cfn-rg-{}", resource.logical_id.to_lowercase()));
let description = props
.get("ReplicationGroupDescription")
.and_then(|v| v.as_str())
.unwrap_or("CFN-provisioned replication group")
.to_string();
let cache_node_type = props
.get("CacheNodeType")
.and_then(|v| v.as_str())
.unwrap_or("cache.t4g.micro")
.to_string();
let engine = props
.get("Engine")
.and_then(|v| v.as_str())
.unwrap_or("redis")
.to_string();
let engine_version = props
.get("EngineVersion")
.and_then(|v| v.as_str())
.unwrap_or("7.1")
.to_string();
let num_cache_clusters = props
.get("NumCacheClusters")
.and_then(|v| v.as_i64())
.map(|n| n as i32)
.unwrap_or(1);
let num_node_groups = props
.get("NumNodeGroups")
.and_then(|v| v.as_i64())
.map(|n| n as i32)
.unwrap_or(0);
let replicas_per_node_group = props
.get("ReplicasPerNodeGroup")
.and_then(|v| v.as_i64())
.map(|n| n as i32);
let automatic_failover_enabled = props
.get("AutomaticFailoverEnabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let multi_az_enabled = props
.get("MultiAZEnabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let transit_encryption_enabled = props
.get("TransitEncryptionEnabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let at_rest_encryption_enabled = props
.get("AtRestEncryptionEnabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let kms_key_id = props
.get("KmsKeyId")
.and_then(|v| v.as_str())
.map(String::from);
let auth_token_enabled = props
.get("AuthToken")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.is_some();
let user_group_ids: Vec<String> = props
.get("UserGroupIds")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let snapshot_retention_limit = props
.get("SnapshotRetentionLimit")
.and_then(|v| v.as_i64())
.map(|n| n as i32)
.unwrap_or(0);
let snapshot_window = props
.get("SnapshotWindow")
.and_then(|v| v.as_str())
.unwrap_or("00:00-01:00")
.to_string();
let port = props
.get("Port")
.and_then(|v| v.as_i64())
.map(|n| n as u16)
.unwrap_or(6379);
let cluster_enabled = num_node_groups > 1
|| props
.get("ClusterEnabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
let arn = format!(
"arn:aws:elasticache:{}:{}:replicationgroup:{}",
state.region, state.account_id, id
);
let endpoint_address = format!(
"{id}.fakecloud.ng.0001.{}.cache.amazonaws.com",
state.region
);
let configuration_endpoint = if cluster_enabled {
Some(format!(
"{id}.fakecloud.cfg.{}.cache.amazonaws.com",
state.region
))
} else {
None
};
let group = EcReplicationGroup {
replication_group_id: id.clone(),
description,
global_replication_group_id: None,
global_replication_group_role: None,
status: "available".to_string(),
cache_node_type,
engine,
engine_version,
num_cache_clusters,
automatic_failover_enabled,
endpoint_address: endpoint_address.clone(),
endpoint_port: port,
arn: arn.clone(),
created_at: Utc::now().to_rfc3339(),
container_id: String::new(),
host_port: 0,
member_clusters: Vec::new(),
snapshot_retention_limit,
snapshot_window,
transit_encryption_enabled,
at_rest_encryption_enabled,
cluster_enabled,
kms_key_id,
auth_token_enabled,
user_group_ids,
multi_az_enabled,
log_delivery_configurations: Vec::new(),
data_tiering: props
.get("DataTieringEnabled")
.and_then(|v| v.as_bool())
.map(|b| if b { "enabled" } else { "disabled" }.to_string()),
ip_discovery: props
.get("IpDiscovery")
.and_then(|v| v.as_str())
.map(String::from),
network_type: props
.get("NetworkType")
.and_then(|v| v.as_str())
.map(String::from),
transit_encryption_mode: props
.get("TransitEncryptionMode")
.and_then(|v| v.as_str())
.map(String::from),
num_node_groups,
configuration_endpoint_address: configuration_endpoint.clone(),
configuration_endpoint_port: configuration_endpoint.as_ref().map(|_| port),
replicas_per_node_group,
auth_token: props
.get("AuthToken")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(String::from),
port,
notification_topic_arn: props
.get("NotificationTopicArn")
.and_then(|v| v.as_str())
.map(String::from),
cluster_mode: props
.get("ClusterMode")
.and_then(|v| v.as_str())
.map(String::from),
data_tiering_enabled: props.get("DataTieringEnabled").and_then(|v| v.as_bool()),
notification_topic_status: None,
cache_parameter_group_name: props
.get("CacheParameterGroupName")
.and_then(|v| v.as_str())
.map(String::from),
cache_subnet_group_name: props
.get("CacheSubnetGroupName")
.and_then(|v| v.as_str())
.map(String::from),
security_group_ids: props
.get("SecurityGroupIds")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default(),
preferred_maintenance_window: props
.get("PreferredMaintenanceWindow")
.and_then(|v| v.as_str())
.map(String::from),
snapshot_name: props
.get("SnapshotName")
.and_then(|v| v.as_str())
.map(String::from),
snapshot_arns: props
.get("SnapshotArns")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default(),
auto_minor_version_upgrade: props
.get("AutoMinorVersionUpgrade")
.and_then(|v| v.as_bool())
.unwrap_or(true),
};
state.replication_groups.insert(id.clone(), group);
let mut result = ProvisionResult::new(id.clone())
.with("Arn", arn)
.with("PrimaryEndPoint.Address", endpoint_address.clone())
.with("PrimaryEndPoint.Port", port.to_string())
.with("ReadEndPoint.Addresses", endpoint_address.clone())
.with("ReadEndPoint.Ports", port.to_string());
if let Some(cfg) = configuration_endpoint {
result = result
.with("ConfigurationEndPoint.Address", cfg)
.with("ConfigurationEndPoint.Port", port.to_string());
}
Ok(result)
}
fn delete_ec_replication_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.elasticache_state.write();
let state = accounts.get_or_create(&self.account_id);
state.replication_groups.remove(physical_id);
Ok(())
}
fn create_route53_hosted_zone(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let normalized_name = if name.ends_with('.') {
name.clone()
} else {
format!("{name}.")
};
let comment = props
.get("HostedZoneConfig")
.and_then(|v| v.get("Comment"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let private_zone = props
.get("VPCs")
.and_then(|v| v.as_array())
.map(|a| !a.is_empty())
.unwrap_or(false);
let vpcs: Vec<fakecloud_route53::model::VPC> = props
.get("VPCs")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|vpc| fakecloud_route53::model::VPC {
vpc_id: vpc.get("VPCId").and_then(|v| v.as_str()).map(String::from),
vpc_region: vpc
.get("VPCRegion")
.and_then(|v| v.as_str())
.map(String::from),
})
.collect()
})
.unwrap_or_default();
let id = format!(
"Z{}",
Uuid::new_v4().simple().to_string()[..14].to_uppercase()
);
let name_servers = (1..=4)
.map(|i| format!("ns-{}.awsdns-{:02}.com", 100 + i, i))
.collect::<Vec<_>>();
let zone = StoredHostedZone {
id: id.clone(),
name: normalized_name,
caller_reference: format!("cfn-{}", resource.logical_id),
comment,
private_zone,
features: Some(HostedZoneFeatures::default()),
vpcs,
delegation_set_id: None,
name_servers: name_servers.clone(),
created_time: Utc::now(),
resource_record_sets: Vec::new(),
};
let mut accounts = self.route53_state.write();
let state = accounts.entry("000000000000");
state.hosted_zones.insert(id.clone(), zone);
let mut result = ProvisionResult::new(id.clone()).with("Id", id);
for (i, ns) in name_servers.iter().enumerate() {
result = result.with(&format!("NameServers.{i}"), ns.clone());
}
result = result.with("NameServers", name_servers.join(","));
Ok(result)
}
fn delete_route53_hosted_zone(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.route53_state.write();
let state = accounts.entry("000000000000");
state.hosted_zones.remove(physical_id);
Ok(())
}
fn create_route53_record_set(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let zone_id = props
.get("HostedZoneId")
.and_then(|v| v.as_str())
.ok_or_else(|| {
"HostedZoneId is required (HostedZoneName lookups not supported)".to_string()
})?
.to_string();
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let normalized_name = if name.ends_with('.') {
name.clone()
} else {
format!("{name}.")
};
let record_type = props
.get("Type")
.and_then(|v| v.as_str())
.ok_or("Type is required")?
.to_string();
let ttl = props.get("TTL").and_then(|v| {
v.as_str()
.and_then(|s| s.parse::<i64>().ok())
.or_else(|| v.as_i64())
});
let resource_records = props
.get("ResourceRecords")
.and_then(|v| v.as_array())
.map(|arr| {
let recs: Vec<fakecloud_route53::model::ResourceRecord> = arr
.iter()
.filter_map(|v| {
v.as_str()
.map(|s| fakecloud_route53::model::ResourceRecord {
value: s.to_string(),
})
})
.collect();
fakecloud_route53::model::ResourceRecords {
resource_record: recs,
}
});
let alias_target =
props
.get("AliasTarget")
.map(|v| fakecloud_route53::model::AliasTarget {
hosted_zone_id: v
.get("HostedZoneId")
.and_then(|x| x.as_str())
.unwrap_or("")
.to_string(),
dns_name: v
.get("DNSName")
.and_then(|x| x.as_str())
.unwrap_or("")
.to_string(),
evaluate_target_health: v
.get("EvaluateTargetHealth")
.and_then(|x| x.as_bool())
.unwrap_or(false),
});
let set_identifier = props
.get("SetIdentifier")
.and_then(|v| v.as_str())
.map(String::from);
let weight = props.get("Weight").and_then(|v| {
v.as_i64()
.or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
});
let region = props
.get("Region")
.and_then(|v| v.as_str())
.map(String::from);
let failover = props
.get("Failover")
.and_then(|v| v.as_str())
.map(String::from);
let multi_value_answer = props.get("MultiValueAnswer").and_then(|v| v.as_bool());
let health_check_id = props
.get("HealthCheckId")
.and_then(|v| v.as_str())
.map(String::from);
let rrset = ResourceRecordSet {
name: normalized_name.clone(),
record_type: record_type.clone(),
set_identifier: set_identifier.clone(),
weight,
region,
geo_location: None,
failover,
multi_value_answer,
ttl,
resource_records,
alias_target,
health_check_id,
traffic_policy_instance_id: None,
cidr_routing_config: None,
geo_proximity_location: None,
};
let mut accounts = self.route53_state.write();
let state = accounts.entry("000000000000");
let zone = state.hosted_zones.get_mut(&zone_id).ok_or_else(|| {
format!(
"HostedZone {zone_id} not yet provisioned; will retry once it has been provisioned"
)
})?;
zone.resource_record_sets.retain(|r| {
!(r.name == rrset.name
&& r.record_type == rrset.record_type
&& r.set_identifier == rrset.set_identifier)
});
zone.resource_record_sets.push(rrset);
let physical_id = match &set_identifier {
Some(sid) => format!("{zone_id}|{normalized_name}|{record_type}|{sid}"),
None => format!("{zone_id}|{normalized_name}|{record_type}"),
};
Ok(ProvisionResult::new(physical_id))
}
fn delete_route53_record_set(
&self,
physical_id: &str,
_attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let parts: Vec<&str> = physical_id.split('|').collect();
if parts.len() < 3 {
return Ok(());
}
let zone_id = parts[0];
let name = parts[1];
let record_type = parts[2];
let set_identifier = parts.get(3).map(|s| s.to_string());
let mut accounts = self.route53_state.write();
let state = accounts.entry("000000000000");
if let Some(zone) = state.hosted_zones.get_mut(zone_id) {
zone.resource_record_sets.retain(|r| {
!(r.name == name
&& r.record_type == record_type
&& r.set_identifier == set_identifier)
});
}
Ok(())
}
fn create_route53_health_check(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let cfg_value = props
.get("HealthCheckConfig")
.ok_or("HealthCheckConfig is required")?;
let health_check_type = cfg_value
.get("Type")
.and_then(|v| v.as_str())
.ok_or("HealthCheckConfig.Type is required")?
.to_string();
let cfg = HealthCheckConfig {
ip_address: cfg_value
.get("IPAddress")
.and_then(|v| v.as_str())
.map(String::from),
port: cfg_value
.get("Port")
.and_then(|v| {
v.as_i64()
.or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
})
.map(|n| n as i32),
health_check_type,
resource_path: cfg_value
.get("ResourcePath")
.and_then(|v| v.as_str())
.map(String::from),
fully_qualified_domain_name: cfg_value
.get("FullyQualifiedDomainName")
.and_then(|v| v.as_str())
.map(String::from),
search_string: cfg_value
.get("SearchString")
.and_then(|v| v.as_str())
.map(String::from),
request_interval: cfg_value
.get("RequestInterval")
.and_then(|v| {
v.as_i64()
.or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
})
.map(|n| n as i32),
failure_threshold: cfg_value
.get("FailureThreshold")
.and_then(|v| {
v.as_i64()
.or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
})
.map(|n| n as i32),
measure_latency: cfg_value.get("MeasureLatency").and_then(|v| v.as_bool()),
inverted: cfg_value.get("Inverted").and_then(|v| v.as_bool()),
disabled: cfg_value.get("Disabled").and_then(|v| v.as_bool()),
health_threshold: cfg_value
.get("HealthThreshold")
.and_then(|v| {
v.as_i64()
.or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
})
.map(|n| n as i32),
child_health_checks: cfg_value
.get("ChildHealthChecks")
.and_then(|v| v.as_array())
.map(|arr| fakecloud_route53::model::ChildHealthChecks {
child_health_check: arr
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
}),
enable_sni: cfg_value.get("EnableSNI").and_then(|v| v.as_bool()),
regions: cfg_value
.get("Regions")
.and_then(|v| v.as_array())
.map(|arr| fakecloud_route53::model::HealthCheckRegions {
region: arr
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
}),
alarm_identifier: cfg_value.get("AlarmIdentifier").map(|v| {
fakecloud_route53::model::AlarmIdentifier {
region: v
.get("Region")
.and_then(|x| x.as_str())
.unwrap_or("")
.to_string(),
name: v
.get("Name")
.and_then(|x| x.as_str())
.unwrap_or("")
.to_string(),
}
}),
insufficient_data_health_status: cfg_value
.get("InsufficientDataHealthStatus")
.and_then(|v| v.as_str())
.map(String::from),
routing_control_arn: cfg_value
.get("RoutingControlArn")
.and_then(|v| v.as_str())
.map(String::from),
};
let id = Uuid::new_v4().to_string();
let hc = StoredHealthCheck {
id: id.clone(),
caller_reference: format!("cfn-{}", resource.logical_id),
version: 1,
config: cfg,
created_time: Utc::now(),
status: fakecloud_route53::HealthCheckStatus::Success,
last_failure_reason: None,
};
let mut accounts = self.route53_state.write();
let state = accounts.entry("000000000000");
state.health_checks.insert(id.clone(), hc);
Ok(ProvisionResult::new(id.clone()).with("HealthCheckId", id))
}
fn delete_route53_health_check(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.route53_state.write();
let state = accounts.entry("000000000000");
state.health_checks.remove(physical_id);
Ok(())
}
fn create_route53_dnssec(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let zone_id = resource
.properties
.get("HostedZoneId")
.and_then(|v| v.as_str())
.ok_or_else(|| "HostedZoneId is required".to_string())?
.trim_start_matches("/hostedzone/")
.to_string();
let mut accounts = self.route53_state.write();
let state = accounts.entry("000000000000");
if !state.hosted_zones.contains_key(&zone_id) {
return Err(format!("HostedZone {zone_id} does not exist"));
}
state
.dnssec_status
.insert(zone_id.clone(), "SIGNING".to_string());
Ok(ProvisionResult::new(zone_id))
}
fn delete_route53_dnssec(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.route53_state.write();
let state = accounts.entry("000000000000");
state.dnssec_status.remove(physical_id);
Ok(())
}
fn create_route53_key_signing_key(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let zone_id = props
.get("HostedZoneId")
.and_then(|v| v.as_str())
.ok_or_else(|| "HostedZoneId is required".to_string())?
.trim_start_matches("/hostedzone/")
.to_string();
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or_else(|| "Name is required".to_string())?
.to_string();
let kms_arn = props
.get("KeyManagementServiceArn")
.and_then(|v| v.as_str())
.ok_or_else(|| "KeyManagementServiceArn is required".to_string())?
.to_string();
let status = props
.get("Status")
.and_then(|v| v.as_str())
.unwrap_or("ACTIVE")
.to_string();
let now = Utc::now();
let key_material = fakecloud_route53::dnssec::derive_keypair(&zone_id, &name);
let key_tag = fakecloud_route53::dnssec::key_tag_for(&key_material.dnskey_public_key);
let mut accounts = self.route53_state.write();
let state = accounts.entry("000000000000");
if !state.hosted_zones.contains_key(&zone_id) {
return Err(format!("HostedZone {zone_id} does not exist"));
}
let zone_name = state
.hosted_zones
.get(&zone_id)
.map(|z| z.name.clone())
.unwrap_or_else(|| ".".to_string());
let ds_digest_hex = fakecloud_route53::dnssec::ds_digest_sha256(
&zone_name,
key_tag,
&key_material.dnskey_public_key,
);
let ksk = fakecloud_route53::StoredKeySigningKey {
hosted_zone_id: zone_id.clone(),
name: name.clone(),
kms_arn,
status,
caller_reference: format!("cfn-{}", Uuid::new_v4()),
created_date: now,
last_modified_date: now,
key_tag: key_tag as i32,
private_key_pem: key_material.private_key_pem,
public_key_der: key_material.public_key_der,
ds_digest_hex,
};
state
.key_signing_keys
.insert((zone_id.clone(), name.clone()), ksk);
Ok(ProvisionResult::new(format!("{zone_id}/{name}")))
}
fn delete_route53_key_signing_key(&self, physical_id: &str) -> Result<(), String> {
let (zone_id, name) = match physical_id.split_once('/') {
Some(parts) => parts,
None => return Ok(()),
};
let mut accounts = self.route53_state.write();
let state = accounts.entry("000000000000");
state
.key_signing_keys
.remove(&(zone_id.to_string(), name.to_string()));
Ok(())
}
fn create_cf_origin_access_identity(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let cfg = props
.get("CloudFrontOriginAccessIdentityConfig")
.ok_or("CloudFrontOriginAccessIdentityConfig is required")?;
let comment = cfg
.get("Comment")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let caller_reference = format!("cfn-{}", resource.logical_id);
let id = format!(
"E{}",
Uuid::new_v4().simple().to_string()[..13].to_uppercase()
);
let etag = format!(
"E{}",
Uuid::new_v4().simple().to_string()[..7].to_uppercase()
);
let s3_canonical_user_id = format!(
"{:0<64}",
Uuid::new_v4().simple().to_string().to_lowercase()
);
let oai = StoredOriginAccessIdentity {
id: id.clone(),
etag,
s3_canonical_user_id: s3_canonical_user_id.clone(),
config: CloudFrontOriginAccessIdentityConfig {
caller_reference,
comment,
},
};
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.origin_access_identities.insert(id.clone(), oai);
Ok(ProvisionResult::new(id.clone())
.with("Id", id)
.with("S3CanonicalUserId", s3_canonical_user_id))
}
fn delete_cf_origin_access_identity(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.origin_access_identities.remove(physical_id);
Ok(())
}
fn create_cf_distribution(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let cfg = resource
.properties
.get("DistributionConfig")
.ok_or_else(|| "DistributionConfig is required".to_string())?;
let origin_entries: Vec<Origin> = cfg
.get("Origins")
.and_then(|v| v.as_array())
.ok_or_else(|| "DistributionConfig.Origins is required".to_string())?
.iter()
.map(|o| {
let mut patched = o.clone();
if let Some(custom) = patched
.get_mut("CustomOriginConfig")
.and_then(|v| v.as_object_mut())
{
if let Some(v) = custom.remove("HTTPPort") {
custom.insert("HttpPort".to_string(), v);
}
if let Some(v) = custom.remove("HTTPSPort") {
custom.insert("HttpsPort".to_string(), v);
}
}
serde_json::from_value::<Origin>(patched)
.map_err(|e| format!("Invalid Origin entry: {e}"))
})
.collect::<Result<Vec<_>, _>>()?;
if origin_entries.is_empty() {
return Err("DistributionConfig.Origins must contain at least one origin".to_string());
}
let origins = Origins {
quantity: origin_entries.len() as i32,
items: Some(OriginItems {
origin: origin_entries,
}),
};
let dcb_value = cfg
.get("DefaultCacheBehavior")
.ok_or_else(|| "DistributionConfig.DefaultCacheBehavior is required".to_string())?;
let default_cache_behavior: DefaultCacheBehavior =
serde_json::from_value(dcb_value.clone())
.map_err(|e| format!("Invalid DefaultCacheBehavior: {e}"))?;
let comment = cfg
.get("Comment")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let enabled = cfg.get("Enabled").and_then(|v| v.as_bool()).unwrap_or(true);
let price_class = cfg
.get("PriceClass")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let http_version = cfg
.get("HttpVersion")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let is_ipv6_enabled = cfg.get("IPV6Enabled").and_then(|v| v.as_bool());
let default_root_object = cfg
.get("DefaultRootObject")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let web_acl_id = cfg
.get("WebACLId")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let viewer_certificate: Option<ViewerCertificate> = cfg
.get("ViewerCertificate")
.map(|v| serde_json::from_value(v.clone()))
.transpose()
.map_err(|e| format!("Invalid ViewerCertificate: {e}"))?;
let caller_reference = format!("cfn-{}-{}", resource.logical_id, Uuid::new_v4().simple());
let mut config = DistributionConfig {
caller_reference,
comment,
enabled,
origins,
default_cache_behavior,
..Default::default()
};
config.price_class = price_class;
config.http_version = http_version;
config.is_ipv6_enabled = is_ipv6_enabled;
config.default_root_object = default_root_object;
config.web_acl_id = web_acl_id;
config.viewer_certificate = viewer_certificate;
let id_suffix: String = Uuid::new_v4()
.simple()
.to_string()
.chars()
.take(13)
.collect::<String>()
.to_uppercase();
let id = format!("E{id_suffix}");
let etag_suffix: String = Uuid::new_v4()
.simple()
.to_string()
.chars()
.take(7)
.collect::<String>()
.to_uppercase();
let etag = format!("E{etag_suffix}");
let domain_name = format!("{}.cloudfront.net", id.to_lowercase());
let arn = format!(
"arn:aws:cloudfront::{}:distribution/{}",
self.account_id, id
);
let stored = StoredDistribution {
id: id.clone(),
arn: arn.clone(),
status: "InProgress".to_string(),
last_modified_time: Utc::now(),
domain_name: domain_name.clone(),
in_progress_invalidation_batches: 0,
etag,
config,
};
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.distributions.insert(id.clone(), stored);
Ok(ProvisionResult::new(id.clone())
.with("Id", id)
.with("DomainName", domain_name)
.with("Arn", arn))
}
fn delete_cf_distribution(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.distributions.remove(physical_id);
Ok(())
}
fn create_cf_origin_access_control(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let cfg = props
.get("OriginAccessControlConfig")
.ok_or("OriginAccessControlConfig is required")?;
let name = cfg
.get("Name")
.and_then(|v| v.as_str())
.ok_or("OriginAccessControlConfig.Name is required")?
.to_string();
let signing_protocol = cfg
.get("SigningProtocol")
.and_then(|v| v.as_str())
.unwrap_or("sigv4")
.to_string();
let signing_behavior = cfg
.get("SigningBehavior")
.and_then(|v| v.as_str())
.unwrap_or("always")
.to_string();
let origin_type = cfg
.get("OriginAccessControlOriginType")
.and_then(|v| v.as_str())
.ok_or("OriginAccessControlConfig.OriginAccessControlOriginType is required")?
.to_string();
let description = cfg
.get("Description")
.and_then(|v| v.as_str())
.map(String::from);
let id = format!(
"E{}",
Uuid::new_v4().simple().to_string()[..13].to_uppercase()
);
let etag = format!(
"E{}",
Uuid::new_v4().simple().to_string()[..7].to_uppercase()
);
let oac = StoredOriginAccessControl {
id: id.clone(),
etag,
config: OriginAccessControlConfig {
name,
description,
signing_protocol,
signing_behavior,
origin_access_control_origin_type: origin_type,
},
};
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.origin_access_controls.insert(id.clone(), oac);
Ok(ProvisionResult::new(id.clone()).with("Id", id))
}
fn delete_cf_origin_access_control(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.origin_access_controls.remove(physical_id);
Ok(())
}
fn create_cf_public_key(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let cfg = props
.get("PublicKeyConfig")
.ok_or("PublicKeyConfig is required")?;
let name = cfg
.get("Name")
.and_then(|v| v.as_str())
.ok_or("PublicKeyConfig.Name is required")?
.to_string();
let encoded_key = cfg
.get("EncodedKey")
.and_then(|v| v.as_str())
.ok_or("PublicKeyConfig.EncodedKey is required")?
.to_string();
let comment = cfg
.get("Comment")
.and_then(|v| v.as_str())
.map(String::from);
let caller_reference = cfg
.get("CallerReference")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let caller_reference = if caller_reference.is_empty() {
format!("cfn-{}", resource.logical_id)
} else {
caller_reference
};
let id = format!(
"K{}",
Uuid::new_v4().simple().to_string()[..13].to_uppercase()
);
let etag = format!(
"E{}",
Uuid::new_v4().simple().to_string()[..7].to_uppercase()
);
let pk = StoredPublicKey {
id: id.clone(),
etag,
created_time: Utc::now(),
config: PublicKeyConfig {
caller_reference,
name,
encoded_key,
comment,
},
};
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.public_keys.insert(id.clone(), pk);
Ok(ProvisionResult::new(id.clone()).with("Id", id))
}
fn delete_cf_public_key(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.public_keys.remove(physical_id);
Ok(())
}
fn create_cf_key_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let cfg = props
.get("KeyGroupConfig")
.ok_or("KeyGroupConfig is required")?;
let name = cfg
.get("Name")
.and_then(|v| v.as_str())
.ok_or("KeyGroupConfig.Name is required")?
.to_string();
let items: Vec<String> = cfg
.get("Items")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let comment = cfg
.get("Comment")
.and_then(|v| v.as_str())
.map(String::from);
let id = format!(
"KG{}",
Uuid::new_v4().simple().to_string()[..12].to_uppercase()
);
let etag = format!(
"E{}",
Uuid::new_v4().simple().to_string()[..7].to_uppercase()
);
let kg = StoredKeyGroup {
id: id.clone(),
etag,
last_modified_time: Utc::now(),
config: KeyGroupConfig {
name,
items: KeyGroupItems { public_key: items },
comment,
},
};
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.key_groups.insert(id.clone(), kg);
Ok(ProvisionResult::new(id.clone()).with("Id", id))
}
fn delete_cf_key_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.key_groups.remove(physical_id);
Ok(())
}
fn create_cf_function(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let function_code = props
.get("FunctionCode")
.and_then(|v| v.as_str())
.ok_or("FunctionCode is required")?
.to_string();
let cfg = props
.get("FunctionConfig")
.ok_or("FunctionConfig is required")?;
let runtime = cfg
.get("Runtime")
.and_then(|v| v.as_str())
.unwrap_or("cloudfront-js-2.0")
.to_string();
let comment = cfg
.get("Comment")
.and_then(|v| v.as_str())
.map(String::from);
let id = format!(
"FN{}",
Uuid::new_v4().simple().to_string()[..12].to_uppercase()
);
let etag = format!(
"E{}",
Uuid::new_v4().simple().to_string()[..7].to_uppercase()
);
let function_arn = format!("arn:aws:cloudfront::{}:function/{}", self.account_id, name);
let now = Utc::now();
let func = StoredFunction {
name: name.clone(),
etag,
status: "UNPUBLISHED".to_string(),
stage: "DEVELOPMENT".to_string(),
function_arn: function_arn.clone(),
created_time: now,
last_modified_time: now,
config: FunctionConfig {
comment,
runtime,
key_value_store_associations: None,
},
function_code,
live_function_code: None,
};
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.functions.insert(name.clone(), func);
Ok(ProvisionResult::new(name.clone())
.with("FunctionARN", function_arn)
.with("FunctionMetadata.FunctionARN", id)
.with("Stage", "DEVELOPMENT"))
}
fn delete_cf_function(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.functions.remove(physical_id);
Ok(())
}
fn create_cf_cache_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let cfg = props
.get("CachePolicyConfig")
.ok_or("CachePolicyConfig is required")?;
let name = cfg
.get("Name")
.and_then(|v| v.as_str())
.ok_or("CachePolicyConfig.Name is required")?
.to_string();
let min_ttl = cfg
.get("MinTTL")
.and_then(|v| {
v.as_i64()
.or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
})
.unwrap_or(0);
let default_ttl = cfg.get("DefaultTTL").and_then(|v| {
v.as_i64()
.or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
});
let max_ttl = cfg.get("MaxTTL").and_then(|v| {
v.as_i64()
.or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
});
let comment = cfg
.get("Comment")
.and_then(|v| v.as_str())
.map(String::from);
let id = format!(
"CP{}",
Uuid::new_v4().simple().to_string()[..12].to_uppercase()
);
let etag = format!(
"E{}",
Uuid::new_v4().simple().to_string()[..7].to_uppercase()
);
let cache_policy = StoredCachePolicy {
id: id.clone(),
etag,
last_modified_time: Utc::now(),
config: CachePolicyConfig {
comment,
name,
default_ttl,
max_ttl,
min_ttl,
parameters_in_cache_key_and_forwarded_to_origin: None,
},
policy_type: "custom".to_string(),
};
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.cache_policies.insert(id.clone(), cache_policy);
Ok(ProvisionResult::new(id.clone()).with("Id", id))
}
fn delete_cf_cache_policy(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.cache_policies.remove(physical_id);
Ok(())
}
fn create_cf_origin_request_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let cfg = props
.get("OriginRequestPolicyConfig")
.ok_or("OriginRequestPolicyConfig is required")?;
let name = cfg
.get("Name")
.and_then(|v| v.as_str())
.ok_or("OriginRequestPolicyConfig.Name is required")?
.to_string();
let header_behavior = cfg
.get("HeadersConfig")
.and_then(|v| v.get("HeaderBehavior"))
.and_then(|v| v.as_str())
.unwrap_or("none")
.to_string();
let cookie_behavior = cfg
.get("CookiesConfig")
.and_then(|v| v.get("CookieBehavior"))
.and_then(|v| v.as_str())
.unwrap_or("none")
.to_string();
let query_string_behavior = cfg
.get("QueryStringsConfig")
.and_then(|v| v.get("QueryStringBehavior"))
.and_then(|v| v.as_str())
.unwrap_or("none")
.to_string();
let comment = cfg
.get("Comment")
.and_then(|v| v.as_str())
.map(String::from);
let id = format!(
"ORP{}",
Uuid::new_v4().simple().to_string()[..11].to_uppercase()
);
let etag = format!(
"E{}",
Uuid::new_v4().simple().to_string()[..7].to_uppercase()
);
let policy = StoredOriginRequestPolicy {
id: id.clone(),
etag,
last_modified_time: Utc::now(),
config: OriginRequestPolicyConfig {
comment,
name,
headers_config: OriginRequestPolicyHeadersConfig {
header_behavior,
headers: None,
},
cookies_config: OriginRequestPolicyCookiesConfig {
cookie_behavior,
cookies: None,
},
query_strings_config: OriginRequestPolicyQueryStringsConfig {
query_string_behavior,
query_strings: None,
},
},
policy_type: "custom".to_string(),
};
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.origin_request_policies.insert(id.clone(), policy);
Ok(ProvisionResult::new(id.clone()).with("Id", id))
}
fn delete_cf_origin_request_policy(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.origin_request_policies.remove(physical_id);
Ok(())
}
fn create_cf_response_headers_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let cfg = props
.get("ResponseHeadersPolicyConfig")
.ok_or("ResponseHeadersPolicyConfig is required")?;
let name = cfg
.get("Name")
.and_then(|v| v.as_str())
.ok_or("ResponseHeadersPolicyConfig.Name is required")?
.to_string();
let comment = cfg
.get("Comment")
.and_then(|v| v.as_str())
.map(String::from);
let id = format!(
"RHP{}",
Uuid::new_v4().simple().to_string()[..11].to_uppercase()
);
let etag = format!(
"E{}",
Uuid::new_v4().simple().to_string()[..7].to_uppercase()
);
let policy = StoredResponseHeadersPolicy {
id: id.clone(),
etag,
last_modified_time: Utc::now(),
config: ResponseHeadersPolicyConfig {
comment,
name,
cors_config: None,
security_headers_config: None,
server_timing_headers_config: None,
custom_headers_config: None,
remove_headers_config: None,
},
policy_type: "custom".to_string(),
};
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.response_headers_policies.insert(id.clone(), policy);
Ok(ProvisionResult::new(id.clone()).with("Id", id))
}
fn delete_cf_response_headers_policy(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.cloudfront_state.write();
let state = accounts.entry("000000000000");
state.response_headers_policies.remove(physical_id);
Ok(())
}
fn create_sfn_state_machine(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("StateMachineName")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| {
let suffix = Uuid::new_v4().simple().to_string();
format!("{}-{}", resource.logical_id, &suffix[..8])
});
let role_arn = props
.get("RoleArn")
.and_then(|v| v.as_str())
.ok_or("RoleArn is required")?
.to_string();
let machine_type_str = props
.get("StateMachineType")
.and_then(|v| v.as_str())
.unwrap_or("STANDARD");
let machine_type = StateMachineType::parse(machine_type_str)
.ok_or_else(|| format!("Invalid StateMachineType: {machine_type_str}"))?;
let definition = props
.get("DefinitionString")
.and_then(|v| v.as_str())
.map(String::from)
.or_else(|| {
props
.get("Definition")
.map(|v| serde_json::to_string(v).unwrap_or_default())
})
.ok_or("Definition or DefinitionString is required")?;
let logging_configuration = props.get("LoggingConfiguration").cloned();
let tracing_configuration = props.get("TracingConfiguration").cloned();
let arn = format!(
"arn:aws:states:{}:{}:stateMachine:{}",
self.region, self.account_id, name
);
let now = Utc::now();
let revision_id = Uuid::new_v4().to_string();
let sm = StateMachine {
name: name.clone(),
arn: arn.clone(),
definition,
role_arn,
machine_type,
status: StateMachineStatus::Active,
creation_date: now,
update_date: now,
tags: BTreeMap::new(),
revision_id,
logging_configuration,
tracing_configuration,
description: String::new(),
};
let mut accounts = self.stepfunctions_state.write();
let state = accounts.get_or_create(&self.account_id);
state.state_machines.insert(arn.clone(), sm);
Ok(ProvisionResult::new(arn.clone())
.with("Arn", arn.clone())
.with("Name", name)
.with("StateMachineRevisionId", "INITIAL"))
}
fn delete_sfn_state_machine(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.stepfunctions_state.write();
let state = accounts.get_or_create(&self.account_id);
state.state_machines.remove(physical_id);
Ok(())
}
fn create_sfn_activity(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let arn = format!(
"arn:aws:states:{}:{}:activity:{}",
self.region, self.account_id, name
);
let activity = SfnActivity {
name: name.clone(),
arn: arn.clone(),
creation_date: Utc::now(),
tags: BTreeMap::new(),
};
let mut accounts = self.stepfunctions_state.write();
let state = accounts.get_or_create(&self.account_id);
state.activities.insert(arn.clone(), activity);
Ok(ProvisionResult::new(arn.clone())
.with("Arn", arn)
.with("Name", name))
}
fn delete_sfn_activity(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.stepfunctions_state.write();
let state = accounts.get_or_create(&self.account_id);
state.activities.remove(physical_id);
Ok(())
}
fn create_sfn_version(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let sm_arn = props
.get("StateMachineArn")
.and_then(|v| v.as_str())
.ok_or("StateMachineArn is required")?
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let revision_id = props
.get("StateMachineRevisionId")
.and_then(|v| v.as_str())
.unwrap_or("INITIAL")
.to_string();
let mut accounts = self.stepfunctions_state.write();
let state = accounts.get_or_create(&self.account_id);
let next_version = state
.state_machine_versions
.values()
.filter(|v| v.state_machine_arn == sm_arn)
.map(|v| v.version)
.max()
.unwrap_or(0)
+ 1;
let version_arn = format!("{sm_arn}:{next_version}");
let version = StateMachineVersion {
state_machine_arn: sm_arn,
version: next_version,
revision_id,
description,
creation_date: Utc::now(),
};
state
.state_machine_versions
.insert(version_arn.clone(), version);
Ok(ProvisionResult::new(version_arn.clone()).with("Arn", version_arn))
}
fn delete_sfn_version(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.stepfunctions_state.write();
let state = accounts.get_or_create(&self.account_id);
state.state_machine_versions.remove(physical_id);
Ok(())
}
fn create_sfn_alias(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let routes_value = props
.get("RoutingConfiguration")
.and_then(|v| v.as_array())
.ok_or("RoutingConfiguration is required")?;
let routing_configuration: Vec<AliasRoute> = routes_value
.iter()
.map(|r| AliasRoute {
state_machine_version_arn: r
.get("StateMachineVersionArn")
.and_then(|x| x.as_str())
.unwrap_or("")
.to_string(),
weight: r
.get("Weight")
.and_then(|x| {
x.as_i64()
.or_else(|| x.as_str().and_then(|s| s.parse::<i64>().ok()))
})
.map(|w| w as i32)
.unwrap_or(0),
})
.collect();
let first_version_arn = routing_configuration
.first()
.map(|r| r.state_machine_version_arn.clone())
.unwrap_or_default();
let sm_arn_root = first_version_arn
.rsplit_once(':')
.map(|(root, _)| root.to_string())
.unwrap_or_else(|| {
format!(
"arn:aws:states:{}:{}:stateMachine:unknown",
self.region, self.account_id
)
});
let arn = format!("{sm_arn_root}:{name}");
let now = Utc::now();
let alias = StateMachineAlias {
name: name.clone(),
arn: arn.clone(),
description,
routing_configuration,
creation_date: now,
update_date: now,
};
let mut accounts = self.stepfunctions_state.write();
let state = accounts.get_or_create(&self.account_id);
state.state_machine_aliases.insert(arn.clone(), alias);
Ok(ProvisionResult::new(arn.clone())
.with("Arn", arn)
.with("Name", name))
}
fn delete_sfn_alias(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.stepfunctions_state.write();
let state = accounts.get_or_create(&self.account_id);
state.state_machine_aliases.remove(physical_id);
Ok(())
}
fn create_wafv2_web_acl(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let scope = props
.get("Scope")
.and_then(|v| v.as_str())
.ok_or("Scope is required")?
.to_string();
let default_action = props
.get("DefaultAction")
.cloned()
.unwrap_or_else(|| serde_json::json!({"Allow": {}}));
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from);
let rules = props
.get("Rules")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let visibility_config = props
.get("VisibilityConfig")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
let capacity = props.get("Capacity").and_then(|v| v.as_i64()).unwrap_or(0);
let id = Uuid::new_v4().to_string();
let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
("us-east-1", "global".to_string())
} else {
(self.region.as_str(), self.region.clone())
};
let arn = format!(
"arn:aws:wafv2:{}:{}:{}/webacl/{}/{}",
region_in_arn, self.account_id, scope_seg, name, id
);
let acl = WebAcl {
id: id.clone(),
name: name.clone(),
arn: arn.clone(),
scope: scope.clone(),
default_action,
description,
rules,
visibility_config,
capacity,
lock_token: Uuid::new_v4().simple().to_string(),
label_namespace: format!("awswaf:{}:webacl:{}:", self.account_id, name),
custom_response_bodies: BTreeMap::new(),
captcha_config: None,
challenge_config: None,
token_domains: Vec::new(),
association_config: None,
data_protection_config: None,
on_source_d_do_s_protection_config: None,
application_config: None,
retrofitted_by_firewall_manager: false,
pre_process_firewall_manager_rule_groups: Vec::new(),
post_process_firewall_manager_rule_groups: Vec::new(),
managed_by_firewall_manager: false,
created_time: Utc::now(),
};
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
state.web_acls.insert((scope.clone(), name.clone()), acl);
Ok(ProvisionResult::new(arn.clone())
.with("Arn", arn)
.with("Id", id)
.with("Name", name)
.with("Capacity", capacity.to_string()))
}
fn delete_wafv2_web_acl(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
state.web_acls.retain(|_, v| v.arn != physical_id);
Ok(())
}
fn create_wafv2_ip_set(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let scope = props
.get("Scope")
.and_then(|v| v.as_str())
.ok_or("Scope is required")?
.to_string();
let ip_address_version = props
.get("IPAddressVersion")
.and_then(|v| v.as_str())
.ok_or("IPAddressVersion is required")?
.to_string();
let addresses: Vec<String> = props
.get("Addresses")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from);
let id = Uuid::new_v4().to_string();
let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
("us-east-1", "global".to_string())
} else {
(self.region.as_str(), self.region.clone())
};
let arn = format!(
"arn:aws:wafv2:{}:{}:{}/ipset/{}/{}",
region_in_arn, self.account_id, scope_seg, name, id
);
let ip_set = IpSet {
id: id.clone(),
name: name.clone(),
arn: arn.clone(),
scope: scope.clone(),
description,
ip_address_version,
addresses,
lock_token: Uuid::new_v4().simple().to_string(),
created_time: Utc::now(),
};
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
state.ip_sets.insert((scope, name.clone()), ip_set);
Ok(ProvisionResult::new(arn.clone())
.with("Arn", arn)
.with("Id", id)
.with("Name", name))
}
fn delete_wafv2_ip_set(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
state.ip_sets.retain(|_, v| v.arn != physical_id);
Ok(())
}
fn create_wafv2_regex_pattern_set(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let scope = props
.get("Scope")
.and_then(|v| v.as_str())
.ok_or("Scope is required")?
.to_string();
let regular_expressions: Vec<serde_json::Value> = props
.get("RegularExpressionList")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|s| {
if let Some(s) = s.as_str() {
serde_json::json!({"RegexString": s})
} else {
s.clone()
}
})
.collect()
})
.unwrap_or_default();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from);
let id = Uuid::new_v4().to_string();
let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
("us-east-1", "global".to_string())
} else {
(self.region.as_str(), self.region.clone())
};
let arn = format!(
"arn:aws:wafv2:{}:{}:{}/regexpatternset/{}/{}",
region_in_arn, self.account_id, scope_seg, name, id
);
let set = RegexPatternSet {
id: id.clone(),
name: name.clone(),
arn: arn.clone(),
scope: scope.clone(),
description,
regular_expressions,
lock_token: Uuid::new_v4().simple().to_string(),
created_time: Utc::now(),
};
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
state.regex_pattern_sets.insert((scope, name.clone()), set);
Ok(ProvisionResult::new(arn.clone())
.with("Arn", arn)
.with("Id", id)
.with("Name", name))
}
fn delete_wafv2_regex_pattern_set(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
state.regex_pattern_sets.retain(|_, v| v.arn != physical_id);
Ok(())
}
fn create_wafv2_rule_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let scope = props
.get("Scope")
.and_then(|v| v.as_str())
.ok_or("Scope is required")?
.to_string();
let capacity = props
.get("Capacity")
.and_then(|v| v.as_i64())
.ok_or("Capacity is required")?;
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from);
let rules = props
.get("Rules")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let visibility_config = props
.get("VisibilityConfig")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
let id = Uuid::new_v4().to_string();
let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
("us-east-1", "global".to_string())
} else {
(self.region.as_str(), self.region.clone())
};
let arn = format!(
"arn:aws:wafv2:{}:{}:{}/rulegroup/{}/{}",
region_in_arn, self.account_id, scope_seg, name, id
);
let rg = RuleGroup {
id: id.clone(),
name: name.clone(),
arn: arn.clone(),
scope: scope.clone(),
capacity,
description,
rules,
visibility_config,
lock_token: Uuid::new_v4().simple().to_string(),
label_namespace: format!("awswaf:{}:rulegroup:{}:", self.account_id, name),
custom_response_bodies: BTreeMap::new(),
available_labels: Vec::new(),
consumed_labels: Vec::new(),
created_time: Utc::now(),
};
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
state.rule_groups.insert((scope, name.clone()), rg);
Ok(ProvisionResult::new(arn.clone())
.with("Arn", arn)
.with("Id", id)
.with("Name", name)
.with("Capacity", capacity.to_string()))
}
fn delete_wafv2_rule_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
state.rule_groups.retain(|_, v| v.arn != physical_id);
Ok(())
}
fn create_wafv2_logging_configuration(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let resource_arn = props
.get("ResourceArn")
.and_then(|v| v.as_str())
.ok_or("ResourceArn is required")?
.to_string();
let cfg = serde_json::json!({
"ResourceArn": resource_arn,
"LogDestinationConfigs": props.get("LogDestinationConfigs").cloned().unwrap_or_else(|| serde_json::json!([])),
"RedactedFields": props.get("RedactedFields").cloned().unwrap_or_else(|| serde_json::json!([])),
"LoggingFilter": props.get("LoggingFilter").cloned(),
});
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
state.logging_configs.insert(resource_arn.clone(), cfg);
Ok(ProvisionResult::new(resource_arn))
}
fn delete_wafv2_logging_configuration(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
state.logging_configs.remove(physical_id);
Ok(())
}
fn create_wafv2_web_acl_association(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let resource_arn = props
.get("ResourceArn")
.and_then(|v| v.as_str())
.ok_or("ResourceArn is required")?
.to_string();
let web_acl_arn = props
.get("WebACLArn")
.and_then(|v| v.as_str())
.ok_or("WebACLArn is required")?
.to_string();
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
state.associations.insert(resource_arn.clone(), web_acl_arn);
Ok(ProvisionResult::new(resource_arn))
}
fn delete_wafv2_web_acl_association(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.wafv2_state.write();
let state = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
state.associations.remove(physical_id);
Ok(())
}
fn create_apigw_rest_api(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from);
let api_key_source = props
.get("ApiKeySourceType")
.and_then(|v| v.as_str())
.unwrap_or("HEADER")
.to_string();
let endpoint_configuration = props
.get("EndpointConfiguration")
.cloned()
.unwrap_or_else(|| serde_json::json!({"types": ["EDGE"]}));
let policy = props
.get("Policy")
.map(|v| v.to_string().trim_matches('"').to_string());
let binary_media_types: Vec<String> = props
.get("BinaryMediaTypes")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let minimum_compression_size = props.get("MinimumCompressionSize").and_then(|v| v.as_i64());
let disable_execute_api_endpoint = props
.get("DisableExecuteApiEndpoint")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let import_source = if props.get("Body").is_some() {
Some("Body".to_string())
} else if props.get("BodyS3Location").is_some() {
Some("BodyS3Location".to_string())
} else if props.get("CloneFrom").is_some() {
Some("CloneFrom".to_string())
} else {
None
};
let tags = parse_acm_tags(props.get("Tags"));
let id = apigw_make_id();
let root_resource_id = apigw_make_id();
let now = Utc::now();
let api = ApiGwRestApi {
id: id.clone(),
name,
description,
version: props
.get("Version")
.and_then(|v| v.as_str())
.map(String::from),
created_date: now,
api_key_source,
endpoint_configuration,
policy,
binary_media_types,
minimum_compression_size,
disable_execute_api_endpoint,
root_resource_id: root_resource_id.clone(),
tags,
import_source,
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state.apis.insert(id.clone(), api);
let mut resources = BTreeMap::new();
resources.insert(
root_resource_id.clone(),
ApiGwResource {
id: root_resource_id.clone(),
parent_id: None,
path_part: None,
path: "/".to_string(),
},
);
state.resources.insert(id.clone(), resources);
Ok(ProvisionResult::new(id.clone())
.with("RestApiId", id.clone())
.with("RootResourceId", root_resource_id))
}
fn update_apigw_rest_api(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let id = existing.physical_id.clone();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let api = state
.apis
.get_mut(&id)
.ok_or_else(|| format!("RestApi {id} not found for update"))?;
if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
api.name = name.to_string();
}
if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
api.description = Some(desc.to_string());
}
if let Some(source) = props.get("ApiKeySourceType").and_then(|v| v.as_str()) {
api.api_key_source = source.to_string();
}
if let Some(ep) = props.get("EndpointConfiguration").cloned() {
api.endpoint_configuration = ep;
}
if let Some(arr) = props.get("BinaryMediaTypes").and_then(|v| v.as_array()) {
api.binary_media_types = arr
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
}
if let Some(size) = props.get("MinimumCompressionSize").and_then(|v| v.as_i64()) {
api.minimum_compression_size = Some(size);
}
if let Some(b) = props
.get("DisableExecuteApiEndpoint")
.and_then(|v| v.as_bool())
{
api.disable_execute_api_endpoint = b;
}
if props.get("Tags").is_some() {
api.tags = parse_acm_tags(props.get("Tags"));
}
let root = api.root_resource_id.clone();
Ok(ProvisionResult::new(id.clone())
.with("RestApiId", id)
.with("RootResourceId", root))
}
fn delete_apigw_rest_api(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state.apis.remove(physical_id);
state.resources.remove(physical_id);
let prefix = format!("{physical_id}/");
state.methods.retain(|k, _| !k.starts_with(&prefix));
state.integrations.retain(|k, _| !k.starts_with(&prefix));
state
.integration_responses
.retain(|k, _| !k.starts_with(&prefix));
state
.method_responses
.retain(|k, _| !k.starts_with(&prefix));
state.deployments.remove(physical_id);
state.stages.remove(physical_id);
state.models.remove(physical_id);
state.request_validators.remove(physical_id);
state.authorizers.remove(physical_id);
state.gateway_responses.remove(physical_id);
Ok(())
}
fn create_apigw_resource(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = props
.get("RestApiId")
.and_then(|v| v.as_str())
.ok_or("RestApiId is required")?
.to_string();
let parent_id = props
.get("ParentId")
.and_then(|v| v.as_str())
.ok_or("ParentId is required")?
.to_string();
let path_part = props
.get("PathPart")
.and_then(|v| v.as_str())
.ok_or("PathPart is required")?
.to_string();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let api_resources = state
.resources
.get(&rest_api_id)
.ok_or_else(|| format!("RestApi {rest_api_id} not found"))?;
let parent = api_resources
.get(&parent_id)
.ok_or_else(|| format!("Parent resource {parent_id} not found"))?;
let parent_path = parent.path.clone();
let path = if parent_path == "/" {
format!("/{path_part}")
} else {
format!("{parent_path}/{path_part}")
};
let id = apigw_make_id();
let new_resource = ApiGwResource {
id: id.clone(),
parent_id: Some(parent_id),
path_part: Some(path_part),
path,
};
state
.resources
.entry(rest_api_id.clone())
.or_default()
.insert(id.clone(), new_resource);
Ok(ProvisionResult::new(id.clone())
.with("ResourceId", id)
.with("RestApiId", rest_api_id))
}
fn delete_apigw_resource(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(rest_api_id) = attributes.get("RestApiId") else {
return Ok(());
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.resources.get_mut(rest_api_id) {
map.remove(physical_id);
}
let prefix = format!("{rest_api_id}/{physical_id}/");
state.methods.retain(|k, _| !k.starts_with(&prefix));
state.integrations.retain(|k, _| !k.starts_with(&prefix));
Ok(())
}
fn create_apigw_method(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = props
.get("RestApiId")
.and_then(|v| v.as_str())
.ok_or("RestApiId is required")?
.to_string();
let resource_id = props
.get("ResourceId")
.and_then(|v| v.as_str())
.ok_or("ResourceId is required")?
.to_string();
let http_method = props
.get("HttpMethod")
.and_then(|v| v.as_str())
.ok_or("HttpMethod is required")?
.to_uppercase();
let authorization_type = props
.get("AuthorizationType")
.and_then(|v| v.as_str())
.unwrap_or("NONE")
.to_string();
let authorizer_id = props
.get("AuthorizerId")
.and_then(|v| v.as_str())
.map(String::from);
let api_key_required = props
.get("ApiKeyRequired")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let operation_name = props
.get("OperationName")
.and_then(|v| v.as_str())
.map(String::from);
let request_validator_id = props
.get("RequestValidatorId")
.and_then(|v| v.as_str())
.map(String::from);
let request_parameters: BTreeMap<String, bool> = props
.get("RequestParameters")
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter()
.map(|(k, v)| (k.clone(), v.as_bool().unwrap_or(false)))
.collect()
})
.unwrap_or_default();
let request_models: BTreeMap<String, String> = props
.get("RequestModels")
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let authorization_scopes: Vec<String> = props
.get("AuthorizationScopes")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let composite_key = format!("{rest_api_id}/{resource_id}/{http_method}");
let method = ApiGwMethod {
rest_api_id: rest_api_id.clone(),
resource_id: resource_id.clone(),
http_method: http_method.clone(),
authorization_type,
authorizer_id,
api_key_required,
operation_name,
request_parameters,
request_models,
request_validator_id,
authorization_scopes,
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.apis.contains_key(&rest_api_id) {
return Err(format!("RestApi {rest_api_id} not found"));
}
let resource_known = state
.resources
.get(&rest_api_id)
.map(|m| m.contains_key(&resource_id))
.unwrap_or(false);
if !resource_known {
return Err(format!(
"Resource {resource_id} not yet provisioned for api {rest_api_id}"
));
}
state.methods.insert(composite_key.clone(), method);
if let Some(integ_props) = props.get("Integration").and_then(|v| v.as_object()) {
let integration = ApiGwIntegration {
rest_api_id: rest_api_id.clone(),
resource_id: resource_id.clone(),
http_method: http_method.clone(),
integration_type: integ_props
.get("Type")
.and_then(|v| v.as_str())
.unwrap_or("MOCK")
.to_string(),
integration_http_method: integ_props
.get("IntegrationHttpMethod")
.and_then(|v| v.as_str())
.map(String::from),
uri: integ_props
.get("Uri")
.and_then(|v| v.as_str())
.map(String::from),
credentials: integ_props
.get("Credentials")
.and_then(|v| v.as_str())
.map(String::from),
request_parameters: integ_props
.get("RequestParameters")
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default(),
request_templates: integ_props
.get("RequestTemplates")
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default(),
passthrough_behavior: integ_props
.get("PassthroughBehavior")
.and_then(|v| v.as_str())
.unwrap_or("WHEN_NO_MATCH")
.to_string(),
timeout_in_millis: integ_props
.get("TimeoutInMillis")
.and_then(|v| v.as_i64())
.map(|n| n as i32),
cache_namespace: integ_props
.get("CacheNamespace")
.and_then(|v| v.as_str())
.map(String::from),
cache_key_parameters: integ_props
.get("CacheKeyParameters")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default(),
content_handling: integ_props
.get("ContentHandling")
.and_then(|v| v.as_str())
.map(String::from),
connection_type: integ_props
.get("ConnectionType")
.and_then(|v| v.as_str())
.map(String::from),
connection_id: integ_props
.get("ConnectionId")
.and_then(|v| v.as_str())
.map(String::from),
tls_config: integ_props.get("TlsConfig").cloned(),
};
state
.integrations
.insert(composite_key.clone(), integration);
}
Ok(ProvisionResult::new(composite_key.clone())
.with("MethodKey", composite_key)
.with("RestApiId", rest_api_id)
.with("ResourceId", resource_id)
.with("HttpMethod", http_method))
}
fn delete_apigw_method(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state.methods.remove(physical_id);
state.integrations.remove(physical_id);
let prefix = format!("{physical_id}/");
state
.integration_responses
.retain(|k, _| !k.starts_with(&prefix));
state
.method_responses
.retain(|k, _| !k.starts_with(&prefix));
Ok(())
}
fn create_apigw_deployment(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = props
.get("RestApiId")
.and_then(|v| v.as_str())
.ok_or("RestApiId is required")?
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from);
let id = apigw_make_id();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.apis.contains_key(&rest_api_id) {
return Err(format!("RestApi {rest_api_id} not found"));
}
let api_summary = serde_json::to_value(
state
.resources
.get(&rest_api_id)
.cloned()
.unwrap_or_default(),
)
.unwrap_or(serde_json::Value::Null);
let deployment = ApiGwDeployment {
id: id.clone(),
description,
created_date: Utc::now(),
api_summary,
};
state
.deployments
.entry(rest_api_id.clone())
.or_default()
.insert(id.clone(), deployment);
if let Some(stage_name) = props
.get("StageName")
.and_then(|v| v.as_str())
.map(String::from)
{
let stage = ApiGwStage {
stage_name: stage_name.clone(),
deployment_id: id.clone(),
description: props
.get("StageDescription")
.and_then(|v| v.get("Description"))
.and_then(|v| v.as_str())
.map(String::from),
cache_cluster_enabled: false,
cache_cluster_size: None,
variables: BTreeMap::new(),
method_settings: BTreeMap::new(),
created_date: Utc::now(),
last_updated_date: Utc::now(),
tracing_enabled: false,
web_acl_arn: None,
canary_settings: None,
access_log_settings: None,
tags: BTreeMap::new(),
};
state
.stages
.entry(rest_api_id.clone())
.or_default()
.insert(stage_name, stage);
}
Ok(ProvisionResult::new(id.clone())
.with("DeploymentId", id)
.with("RestApiId", rest_api_id))
}
fn delete_apigw_deployment(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(rest_api_id) = attributes.get("RestApiId") else {
return Ok(());
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.deployments.get_mut(rest_api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigw_stage(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = props
.get("RestApiId")
.and_then(|v| v.as_str())
.ok_or("RestApiId is required")?
.to_string();
let stage_name = props
.get("StageName")
.and_then(|v| v.as_str())
.ok_or("StageName is required")?
.to_string();
let deployment_id = props
.get("DeploymentId")
.and_then(|v| v.as_str())
.ok_or("DeploymentId is required")?
.to_string();
let variables: BTreeMap<String, String> = props
.get("Variables")
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let tracing_enabled = props
.get("TracingEnabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let cache_cluster_enabled = props
.get("CacheClusterEnabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let cache_cluster_size = props
.get("CacheClusterSize")
.and_then(|v| v.as_str())
.map(String::from);
let method_settings: BTreeMap<String, serde_json::Value> = props
.get("MethodSettings")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|s| {
let path = s.get("ResourcePath").and_then(|v| v.as_str())?;
let http = s.get("HttpMethod").and_then(|v| v.as_str())?;
let key = format!("{}/{http}", path.strip_prefix('/').unwrap_or(path));
Some((key, s.clone()))
})
.collect()
})
.unwrap_or_default();
let tags = parse_acm_tags(props.get("Tags"));
let stage = ApiGwStage {
stage_name: stage_name.clone(),
deployment_id,
description: props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from),
cache_cluster_enabled,
cache_cluster_size,
variables,
method_settings,
created_date: Utc::now(),
last_updated_date: Utc::now(),
tracing_enabled,
web_acl_arn: None,
canary_settings: props.get("CanarySetting").cloned(),
access_log_settings: props.get("AccessLogSetting").cloned(),
tags,
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.apis.contains_key(&rest_api_id) {
return Err(format!("RestApi {rest_api_id} not found"));
}
let dep_known = state
.deployments
.get(&rest_api_id)
.map(|m| m.contains_key(&stage.deployment_id))
.unwrap_or(false);
if !dep_known {
return Err(format!(
"Deployment {} not yet provisioned for api {rest_api_id}",
stage.deployment_id
));
}
state
.stages
.entry(rest_api_id.clone())
.or_default()
.insert(stage_name.clone(), stage);
Ok(ProvisionResult::new(stage_name.clone())
.with("StageName", stage_name)
.with("RestApiId", rest_api_id))
}
fn delete_apigw_stage(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(rest_api_id) = attributes.get("RestApiId") else {
return Ok(());
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.stages.get_mut(rest_api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigw_authorizer(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = props
.get("RestApiId")
.and_then(|v| v.as_str())
.ok_or("RestApiId is required")?
.to_string();
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let authorizer_type = props
.get("Type")
.and_then(|v| v.as_str())
.unwrap_or("TOKEN")
.to_string();
let provider_arns: Vec<String> = props
.get("ProviderARNs")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let id = apigw_make_id();
let auth = ApiGwAuthorizer {
id: id.clone(),
name,
authorizer_type,
provider_arns,
auth_type: props
.get("AuthType")
.and_then(|v| v.as_str())
.map(String::from),
authorizer_uri: props
.get("AuthorizerUri")
.and_then(|v| v.as_str())
.map(String::from),
authorizer_credentials: props
.get("AuthorizerCredentials")
.and_then(|v| v.as_str())
.map(String::from),
identity_source: props
.get("IdentitySource")
.and_then(|v| v.as_str())
.map(String::from),
identity_validation_expression: props
.get("IdentityValidationExpression")
.and_then(|v| v.as_str())
.map(String::from),
authorizer_result_ttl_in_seconds: props
.get("AuthorizerResultTtlInSeconds")
.and_then(|v| v.as_i64())
.map(|n| n as i32),
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.apis.contains_key(&rest_api_id) {
return Err(format!("RestApi {rest_api_id} not found"));
}
state
.authorizers
.entry(rest_api_id.clone())
.or_default()
.insert(id.clone(), auth);
Ok(ProvisionResult::new(id.clone())
.with("AuthorizerId", id)
.with("RestApiId", rest_api_id))
}
fn delete_apigw_authorizer(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(rest_api_id) = attributes.get("RestApiId") else {
return Ok(());
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.authorizers.get_mut(rest_api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigw_request_validator(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = props
.get("RestApiId")
.and_then(|v| v.as_str())
.ok_or("RestApiId is required")?
.to_string();
let name = props.get("Name").and_then(|v| v.as_str()).map(String::from);
let validate_body = props
.get("ValidateRequestBody")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let validate_params = props
.get("ValidateRequestParameters")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let id = apigw_make_id();
let body = serde_json::json!({
"id": id,
"name": name,
"validateRequestBody": validate_body,
"validateRequestParameters": validate_params,
});
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state
.request_validators
.entry(rest_api_id.clone())
.or_default()
.insert(id.clone(), body);
Ok(ProvisionResult::new(id.clone())
.with("RequestValidatorId", id)
.with("RestApiId", rest_api_id))
}
fn delete_apigw_request_validator(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(rest_api_id) = attributes.get("RestApiId") else {
return Ok(());
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.request_validators.get_mut(rest_api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigw_model(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = props
.get("RestApiId")
.and_then(|v| v.as_str())
.ok_or("RestApiId is required")?
.to_string();
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let content_type = props
.get("ContentType")
.and_then(|v| v.as_str())
.unwrap_or("application/json")
.to_string();
let schema = props.get("Schema").map(|v| {
if let Some(s) = v.as_str() {
s.to_string()
} else {
v.to_string()
}
});
let id = apigw_make_id();
let model = ApiGwModel {
id: id.clone(),
name: name.clone(),
description: props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from),
schema,
content_type,
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state
.models
.entry(rest_api_id.clone())
.or_default()
.insert(name.clone(), model);
Ok(ProvisionResult::new(name.clone())
.with("ModelName", name)
.with("RestApiId", rest_api_id))
}
fn delete_apigw_model(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(rest_api_id) = attributes.get("RestApiId") else {
return Ok(());
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.models.get_mut(rest_api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigw_gateway_response(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = props
.get("RestApiId")
.and_then(|v| v.as_str())
.ok_or("RestApiId is required")?
.to_string();
let response_type = props
.get("ResponseType")
.and_then(|v| v.as_str())
.ok_or("ResponseType is required")?
.to_string();
let body = serde_json::json!({
"responseType": response_type,
"statusCode": props.get("StatusCode").and_then(|v| v.as_str()),
"responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
"responseTemplates": props.get("ResponseTemplates").cloned().unwrap_or(serde_json::json!({})),
});
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state
.gateway_responses
.entry(rest_api_id.clone())
.or_default()
.insert(response_type.clone(), body);
Ok(ProvisionResult::new(response_type.clone())
.with("ResponseType", response_type)
.with("RestApiId", rest_api_id))
}
fn delete_apigw_gateway_response(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(rest_api_id) = attributes.get("RestApiId") else {
return Ok(());
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.gateway_responses.get_mut(rest_api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigw_usage_plan(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("UsagePlanName")
.and_then(|v| v.as_str())
.ok_or("UsagePlanName is required")?
.to_string();
let id = apigw_make_id();
let plan = ApiGwUsagePlan {
id: id.clone(),
name,
description: props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from),
api_stages: props
.get("ApiStages")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default()
.into_iter()
.map(lowercase_first_keys)
.collect(),
throttle: props.get("Throttle").cloned().map(lowercase_first_keys),
quota: props.get("Quota").cloned().map(lowercase_first_keys),
product_code: None,
tags: parse_acm_tags(props.get("Tags")),
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state.usage_plans.insert(id.clone(), plan);
Ok(ProvisionResult::new(id.clone()).with("UsagePlanId", id))
}
fn delete_apigw_usage_plan(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state.usage_plans.remove(physical_id);
state.usage_plan_keys.remove(physical_id);
Ok(())
}
fn create_apigw_api_key(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let generate_distinct_id = props
.get("GenerateDistinctId")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let name = props
.get("Name")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| {
if generate_distinct_id {
format!("cfn-key-{}-{}", resource.logical_id, apigw_make_id())
} else {
format!("cfn-key-{}", resource.logical_id)
}
});
let value = props
.get("Value")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| Uuid::new_v4().simple().to_string());
let enabled = props
.get("Enabled")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let stage_keys: Vec<String> = props
.get("StageKeys")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|s| {
let rest = s.get("RestApiId").and_then(|v| v.as_str())?;
let stage = s.get("StageName").and_then(|v| v.as_str())?;
Some(format!("{rest}/{stage}"))
})
.collect()
})
.unwrap_or_default();
let id = apigw_make_id();
let now = Utc::now();
let key = ApiGwApiKey {
id: id.clone(),
value,
name,
description: props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from),
enabled,
created_date: now,
last_updated_date: now,
stage_keys,
tags: parse_acm_tags(props.get("Tags")),
customer_id: props
.get("CustomerId")
.and_then(|v| v.as_str())
.map(String::from),
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state.api_keys.insert(id.clone(), key);
Ok(ProvisionResult::new(id.clone()).with("ApiKeyId", id))
}
fn delete_apigw_api_key(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state.api_keys.remove(physical_id);
Ok(())
}
fn create_apigw_usage_plan_key(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let usage_plan_id = props
.get("UsagePlanId")
.and_then(|v| v.as_str())
.ok_or("UsagePlanId is required")?
.to_string();
let key_id = props
.get("KeyId")
.and_then(|v| v.as_str())
.ok_or("KeyId is required")?
.to_string();
let key_type = props
.get("KeyType")
.and_then(|v| v.as_str())
.unwrap_or("API_KEY")
.to_string();
let body = serde_json::json!({
"id": key_id,
"type": key_type,
});
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.usage_plans.contains_key(&usage_plan_id) {
return Err(format!("UsagePlan {usage_plan_id} not yet provisioned"));
}
if !state.api_keys.contains_key(&key_id) {
return Err(format!("ApiKey {key_id} not yet provisioned"));
}
state
.usage_plan_keys
.entry(usage_plan_id.clone())
.or_default()
.insert(key_id.clone(), body);
let physical = format!("{usage_plan_id}/{key_id}");
Ok(ProvisionResult::new(physical)
.with("UsagePlanId", usage_plan_id)
.with("KeyId", key_id))
}
fn delete_apigw_usage_plan_key(
&self,
physical_id: &str,
_attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let mut parts = physical_id.splitn(2, '/');
let Some(plan_id) = parts.next() else {
return Ok(());
};
let Some(key_id) = parts.next() else {
return Ok(());
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.usage_plan_keys.get_mut(plan_id) {
map.remove(key_id);
}
Ok(())
}
fn create_apigw_domain_name(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let domain_name = props
.get("DomainName")
.and_then(|v| v.as_str())
.ok_or("DomainName is required")?
.to_string();
let mtls = props
.get("MutualTlsAuthentication")
.cloned()
.map(lowercase_first_keys);
let regional_domain = format!(
"d-{}.execute-api.{}.amazonaws.com",
apigw_make_id(),
self.region
);
let distribution_domain = format!("d{}.cloudfront.net", apigw_make_id());
let body = serde_json::json!({
"domainName": domain_name,
"certificateArn": props.get("CertificateArn").and_then(|v| v.as_str()),
"regionalCertificateArn": props.get("RegionalCertificateArn").and_then(|v| v.as_str()),
"endpointConfiguration": props.get("EndpointConfiguration").cloned().unwrap_or(serde_json::json!({"types": ["EDGE"]})),
"securityPolicy": props.get("SecurityPolicy").and_then(|v| v.as_str()),
"ownershipVerificationCertificateArn": props.get("OwnershipVerificationCertificateArn").and_then(|v| v.as_str()),
"regionalDomainName": regional_domain,
"regionalHostedZoneId": "Z2FDTNDATAQYW2",
"distributionDomainName": distribution_domain,
"distributionHostedZoneId": "Z2FDTNDATAQYW2",
"mutualTlsAuthentication": mtls,
"tags": serde_json::Value::Object(
parse_acm_tags(props.get("Tags"))
.into_iter()
.map(|(k, v)| (k, serde_json::Value::String(v)))
.collect(),
),
});
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state.domain_names.insert(domain_name.clone(), body);
Ok(ProvisionResult::new(domain_name.clone())
.with("DomainName", domain_name)
.with("RegionalHostedZoneId", "Z2FDTNDATAQYW2".to_string())
.with("DistributionHostedZoneId", "Z2FDTNDATAQYW2".to_string()))
}
fn delete_apigw_domain_name(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state.domain_names.remove(physical_id);
state.base_path_mappings.remove(physical_id);
Ok(())
}
fn create_apigw_base_path_mapping(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let domain_name = props
.get("DomainName")
.and_then(|v| v.as_str())
.ok_or("DomainName is required")?
.to_string();
let rest_api_id = props
.get("RestApiId")
.and_then(|v| v.as_str())
.ok_or("RestApiId is required")?
.to_string();
let base_path = props
.get("BasePath")
.and_then(|v| v.as_str())
.unwrap_or("(none)")
.to_string();
let stage = props
.get("Stage")
.and_then(|v| v.as_str())
.map(String::from);
let body = serde_json::json!({
"basePath": base_path,
"restApiId": rest_api_id,
"stage": stage,
});
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
state
.base_path_mappings
.entry(domain_name.clone())
.or_default()
.insert(base_path.clone(), body);
let physical = format!("{domain_name}/{base_path}");
Ok(ProvisionResult::new(physical)
.with("DomainName", domain_name)
.with("BasePath", base_path))
}
fn delete_apigw_base_path_mapping(
&self,
physical_id: &str,
_attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let mut parts = physical_id.splitn(2, '/');
let Some(domain) = parts.next() else {
return Ok(());
};
let Some(base_path) = parts.next() else {
return Ok(());
};
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.base_path_mappings.get_mut(domain) {
map.remove(base_path);
}
Ok(())
}
fn update_apigw_resource(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = existing
.attributes
.get("RestApiId")
.cloned()
.or_else(|| {
props
.get("RestApiId")
.and_then(|v| v.as_str())
.map(String::from)
})
.ok_or("RestApiId is required")?;
let physical = existing.physical_id.clone();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let api_resources = state
.resources
.get_mut(&rest_api_id)
.ok_or_else(|| format!("RestApi {rest_api_id} not found"))?;
if !api_resources.contains_key(&physical) {
return Err(format!("Resource {physical} not found"));
}
if let Some(part) = props.get("PathPart").and_then(|v| v.as_str()) {
let parent_id = api_resources
.get(&physical)
.and_then(|r| r.parent_id.clone());
let parent_path = parent_id
.as_ref()
.and_then(|pid| api_resources.get(pid).map(|p| p.path.clone()))
.unwrap_or_else(|| "/".to_string());
let new_path = if parent_path == "/" {
format!("/{part}")
} else {
format!("{parent_path}/{part}")
};
let res = api_resources
.get_mut(&physical)
.expect("contains_key checked above");
res.path_part = Some(part.to_string());
res.path = new_path;
}
Ok(ProvisionResult::new(physical.clone())
.with("ResourceId", physical)
.with("RestApiId", rest_api_id))
}
fn update_apigw_method(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
self.create_apigw_method(resource).map(|r| {
ProvisionResult {
physical_id: existing.physical_id.clone(),
attributes: r.attributes,
}
})
}
fn update_apigw_deployment(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = existing
.attributes
.get("RestApiId")
.cloned()
.or_else(|| {
props
.get("RestApiId")
.and_then(|v| v.as_str())
.map(String::from)
})
.ok_or("RestApiId is required")?;
let physical = existing.physical_id.clone();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let dep = state
.deployments
.get_mut(&rest_api_id)
.and_then(|m| m.get_mut(&physical))
.ok_or_else(|| format!("Deployment {physical} not found"))?;
if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
dep.description = Some(desc.to_string());
}
Ok(ProvisionResult::new(physical.clone())
.with("DeploymentId", physical)
.with("RestApiId", rest_api_id))
}
fn update_apigw_stage(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = existing
.attributes
.get("RestApiId")
.cloned()
.or_else(|| {
props
.get("RestApiId")
.and_then(|v| v.as_str())
.map(String::from)
})
.ok_or("RestApiId is required")?;
let stage_name = existing.physical_id.clone();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let stage = state
.stages
.get_mut(&rest_api_id)
.and_then(|m| m.get_mut(&stage_name))
.ok_or_else(|| format!("Stage {stage_name} not found"))?;
if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
stage.description = Some(desc.to_string());
}
if let Some(b) = props.get("TracingEnabled").and_then(|v| v.as_bool()) {
stage.tracing_enabled = b;
}
if let Some(b) = props.get("CacheClusterEnabled").and_then(|v| v.as_bool()) {
stage.cache_cluster_enabled = b;
}
if let Some(s) = props.get("CacheClusterSize").and_then(|v| v.as_str()) {
stage.cache_cluster_size = Some(s.to_string());
}
if let Some(obj) = props.get("Variables").and_then(|v| v.as_object()) {
stage.variables = obj
.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect();
}
if let Some(dep) = props.get("DeploymentId").and_then(|v| v.as_str()) {
stage.deployment_id = dep.to_string();
}
if props.get("Tags").is_some() {
stage.tags = parse_acm_tags(props.get("Tags"));
}
if let Some(arr) = props.get("MethodSettings").and_then(|v| v.as_array()) {
stage.method_settings = arr
.iter()
.filter_map(|s| {
let path = s.get("ResourcePath").and_then(|v| v.as_str())?;
let http = s.get("HttpMethod").and_then(|v| v.as_str())?;
let key = format!("{}/{http}", path.strip_prefix('/').unwrap_or(path));
Some((key, s.clone()))
})
.collect();
}
if let Some(canary) = props.get("CanarySetting").cloned() {
stage.canary_settings = Some(canary);
}
if let Some(access) = props.get("AccessLogSetting").cloned() {
stage.access_log_settings = Some(access);
}
stage.last_updated_date = Utc::now();
Ok(ProvisionResult::new(stage_name.clone())
.with("StageName", stage_name)
.with("RestApiId", rest_api_id))
}
fn update_apigw_authorizer(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = existing
.attributes
.get("RestApiId")
.cloned()
.or_else(|| {
props
.get("RestApiId")
.and_then(|v| v.as_str())
.map(String::from)
})
.ok_or("RestApiId is required")?;
let physical = existing.physical_id.clone();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let auth = state
.authorizers
.get_mut(&rest_api_id)
.and_then(|m| m.get_mut(&physical))
.ok_or_else(|| format!("Authorizer {physical} not found"))?;
if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
auth.name = name.to_string();
}
if let Some(t) = props.get("Type").and_then(|v| v.as_str()) {
auth.authorizer_type = t.to_string();
}
if let Some(uri) = props.get("AuthorizerUri").and_then(|v| v.as_str()) {
auth.authorizer_uri = Some(uri.to_string());
}
if let Some(arr) = props.get("ProviderARNs").and_then(|v| v.as_array()) {
auth.provider_arns = arr
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
}
if let Some(s) = props.get("IdentitySource").and_then(|v| v.as_str()) {
auth.identity_source = Some(s.to_string());
}
if let Some(s) = props
.get("IdentityValidationExpression")
.and_then(|v| v.as_str())
{
auth.identity_validation_expression = Some(s.to_string());
}
if let Some(n) = props
.get("AuthorizerResultTtlInSeconds")
.and_then(|v| v.as_i64())
{
auth.authorizer_result_ttl_in_seconds = Some(n as i32);
}
if let Some(s) = props.get("AuthType").and_then(|v| v.as_str()) {
auth.auth_type = Some(s.to_string());
}
if let Some(s) = props.get("AuthorizerCredentials").and_then(|v| v.as_str()) {
auth.authorizer_credentials = Some(s.to_string());
}
Ok(ProvisionResult::new(physical.clone())
.with("AuthorizerId", physical)
.with("RestApiId", rest_api_id))
}
fn update_apigw_request_validator(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = existing
.attributes
.get("RestApiId")
.cloned()
.or_else(|| {
props
.get("RestApiId")
.and_then(|v| v.as_str())
.map(String::from)
})
.ok_or("RestApiId is required")?;
let physical = existing.physical_id.clone();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let body = state
.request_validators
.get_mut(&rest_api_id)
.and_then(|m| m.get_mut(&physical))
.ok_or_else(|| format!("RequestValidator {physical} not found"))?;
let obj = body.as_object_mut().ok_or("validator body not object")?;
if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
obj.insert("name".into(), serde_json::Value::String(name.into()));
}
if let Some(b) = props.get("ValidateRequestBody").and_then(|v| v.as_bool()) {
obj.insert("validateRequestBody".into(), serde_json::Value::Bool(b));
}
if let Some(b) = props
.get("ValidateRequestParameters")
.and_then(|v| v.as_bool())
{
obj.insert(
"validateRequestParameters".into(),
serde_json::Value::Bool(b),
);
}
Ok(ProvisionResult::new(physical.clone())
.with("RequestValidatorId", physical)
.with("RestApiId", rest_api_id))
}
fn update_apigw_model(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = existing
.attributes
.get("RestApiId")
.cloned()
.or_else(|| {
props
.get("RestApiId")
.and_then(|v| v.as_str())
.map(String::from)
})
.ok_or("RestApiId is required")?;
let model_name = existing.physical_id.clone();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let model = state
.models
.get_mut(&rest_api_id)
.and_then(|m| m.get_mut(&model_name))
.ok_or_else(|| format!("Model {model_name} not found"))?;
if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
model.description = Some(desc.to_string());
}
if let Some(s) = props.get("ContentType").and_then(|v| v.as_str()) {
model.content_type = s.to_string();
}
if let Some(schema) = props.get("Schema") {
model.schema = Some(if let Some(s) = schema.as_str() {
s.to_string()
} else {
schema.to_string()
});
}
Ok(ProvisionResult::new(model_name.clone())
.with("ModelName", model_name)
.with("RestApiId", rest_api_id))
}
fn update_apigw_gateway_response(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rest_api_id = existing
.attributes
.get("RestApiId")
.cloned()
.or_else(|| {
props
.get("RestApiId")
.and_then(|v| v.as_str())
.map(String::from)
})
.ok_or("RestApiId is required")?;
let response_type = existing.physical_id.clone();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let body = state
.gateway_responses
.get_mut(&rest_api_id)
.and_then(|m| m.get_mut(&response_type))
.ok_or_else(|| format!("GatewayResponse {response_type} not found"))?;
let obj = body.as_object_mut().ok_or("response body not object")?;
if let Some(s) = props.get("StatusCode").and_then(|v| v.as_str()) {
obj.insert("statusCode".into(), serde_json::Value::String(s.into()));
}
if let Some(v) = props.get("ResponseParameters").cloned() {
obj.insert("responseParameters".into(), v);
}
if let Some(v) = props.get("ResponseTemplates").cloned() {
obj.insert("responseTemplates".into(), v);
}
Ok(ProvisionResult::new(response_type.clone())
.with("ResponseType", response_type)
.with("RestApiId", rest_api_id))
}
fn update_apigw_usage_plan(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let physical = existing.physical_id.clone();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let plan = state
.usage_plans
.get_mut(&physical)
.ok_or_else(|| format!("UsagePlan {physical} not found"))?;
if let Some(name) = props.get("UsagePlanName").and_then(|v| v.as_str()) {
plan.name = name.to_string();
}
if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
plan.description = Some(s.to_string());
}
if let Some(arr) = props.get("ApiStages").and_then(|v| v.as_array()) {
plan.api_stages = arr.iter().cloned().map(lowercase_first_keys).collect();
}
if let Some(t) = props.get("Throttle").cloned() {
plan.throttle = Some(lowercase_first_keys(t));
}
if let Some(q) = props.get("Quota").cloned() {
plan.quota = Some(lowercase_first_keys(q));
}
if props.get("Tags").is_some() {
plan.tags = parse_acm_tags(props.get("Tags"));
}
Ok(ProvisionResult::new(physical.clone()).with("UsagePlanId", physical))
}
fn update_apigw_api_key(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let physical = existing.physical_id.clone();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let key = state
.api_keys
.get_mut(&physical)
.ok_or_else(|| format!("ApiKey {physical} not found"))?;
if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
key.name = name.to_string();
}
if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
key.description = Some(s.to_string());
}
if let Some(b) = props.get("Enabled").and_then(|v| v.as_bool()) {
key.enabled = b;
}
if let Some(s) = props.get("CustomerId").and_then(|v| v.as_str()) {
key.customer_id = Some(s.to_string());
}
if props.get("Tags").is_some() {
key.tags = parse_acm_tags(props.get("Tags"));
}
if let Some(arr) = props.get("StageKeys").and_then(|v| v.as_array()) {
key.stage_keys = arr
.iter()
.filter_map(|s| {
let rest = s.get("RestApiId").and_then(|v| v.as_str())?;
let stage = s.get("StageName").and_then(|v| v.as_str())?;
Some(format!("{rest}/{stage}"))
})
.collect();
}
key.last_updated_date = Utc::now();
Ok(ProvisionResult::new(physical.clone()).with("ApiKeyId", physical))
}
fn update_apigw_usage_plan_key(
&self,
existing: &StackResource,
_resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let physical = existing.physical_id.clone();
let mut parts = physical.splitn(2, '/');
let plan = parts.next().unwrap_or("").to_string();
let key = parts.next().unwrap_or("").to_string();
Ok(ProvisionResult::new(physical)
.with("UsagePlanId", plan)
.with("KeyId", key))
}
fn update_apigw_domain_name(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let domain = existing.physical_id.clone();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let body = state
.domain_names
.get_mut(&domain)
.ok_or_else(|| format!("DomainName {domain} not found"))?;
let obj = body.as_object_mut().ok_or("domain body not object")?;
if let Some(s) = props.get("CertificateArn").and_then(|v| v.as_str()) {
obj.insert("certificateArn".into(), serde_json::Value::String(s.into()));
}
if let Some(s) = props.get("RegionalCertificateArn").and_then(|v| v.as_str()) {
obj.insert(
"regionalCertificateArn".into(),
serde_json::Value::String(s.into()),
);
}
if let Some(v) = props.get("EndpointConfiguration").cloned() {
obj.insert("endpointConfiguration".into(), v);
}
if let Some(s) = props.get("SecurityPolicy").and_then(|v| v.as_str()) {
obj.insert("securityPolicy".into(), serde_json::Value::String(s.into()));
}
if let Some(v) = props.get("MutualTlsAuthentication").cloned() {
obj.insert("mutualTlsAuthentication".into(), lowercase_first_keys(v));
}
if let Some(s) = props
.get("OwnershipVerificationCertificateArn")
.and_then(|v| v.as_str())
{
obj.insert(
"ownershipVerificationCertificateArn".into(),
serde_json::Value::String(s.into()),
);
}
if props.get("Tags").is_some() {
obj.insert(
"tags".into(),
serde_json::Value::Object(
parse_acm_tags(props.get("Tags"))
.into_iter()
.map(|(k, v)| (k, serde_json::Value::String(v)))
.collect(),
),
);
}
Ok(ProvisionResult::new(domain.clone())
.with("DomainName", domain)
.with("RegionalHostedZoneId", "Z2FDTNDATAQYW2".to_string())
.with("DistributionHostedZoneId", "Z2FDTNDATAQYW2".to_string()))
}
fn update_apigw_base_path_mapping(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let physical = existing.physical_id.clone();
let mut parts = physical.splitn(2, '/');
let domain = parts
.next()
.ok_or("malformed base path mapping id")?
.to_string();
let base_path = parts
.next()
.ok_or("malformed base path mapping id")?
.to_string();
let mut accounts = self.apigateway_state.write();
let state = accounts.get_or_create(&self.account_id);
let map = state
.base_path_mappings
.get_mut(&domain)
.ok_or_else(|| format!("DomainName {domain} not found"))?;
let body = map
.get_mut(&base_path)
.ok_or_else(|| format!("BasePath {base_path} not found"))?;
let obj = body.as_object_mut().ok_or("mapping body not object")?;
if let Some(s) = props.get("RestApiId").and_then(|v| v.as_str()) {
obj.insert("restApiId".into(), serde_json::Value::String(s.into()));
}
if let Some(s) = props.get("Stage").and_then(|v| v.as_str()) {
obj.insert("stage".into(), serde_json::Value::String(s.into()));
}
Ok(ProvisionResult::new(physical)
.with("DomainName", domain)
.with("BasePath", base_path))
}
fn create_apigwv2_api(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let protocol_type = props
.get("ProtocolType")
.and_then(|v| v.as_str())
.unwrap_or("HTTP")
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from);
let tags: Option<BTreeMap<String, String>> =
props.get("Tags").and_then(|v| v.as_object()).map(|obj| {
obj.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
});
let id = make_apigwv2_id(10);
let mut api = ApiGwV2HttpApi::new(id.clone(), name, description, tags, &self.region);
api.protocol_type = protocol_type.clone();
if let Some(expr) = props
.get("RouteSelectionExpression")
.and_then(|v| v.as_str())
{
api.route_selection_expression = expr.to_string();
}
if let Some(expr) = props
.get("ApiKeySelectionExpression")
.and_then(|v| v.as_str())
{
api.api_key_selection_expression = expr.to_string();
}
if let Some(b) = props
.get("DisableExecuteApiEndpoint")
.and_then(|v| v.as_bool())
{
api.disable_execute_api_endpoint = b;
}
if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
api.ip_address_type = s.to_string();
}
if let Some(cors) = props.get("CorsConfiguration").and_then(|v| v.as_object()) {
api.cors_configuration = Some(ApiGwV2CorsConfiguration {
allow_credentials: cors.get("AllowCredentials").and_then(|v| v.as_bool()),
allow_headers: cors
.get("AllowHeaders")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}),
allow_methods: cors
.get("AllowMethods")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}),
allow_origins: cors
.get("AllowOrigins")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}),
expose_headers: cors
.get("ExposeHeaders")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}),
max_age: cors
.get("MaxAge")
.and_then(|v| v.as_i64())
.map(|n| n as i32),
});
}
let api_endpoint = api.api_endpoint.clone();
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
state.apis.insert(id.clone(), api);
Ok(ProvisionResult::new(id.clone())
.with("ApiId", id)
.with("ApiEndpoint", api_endpoint))
}
fn delete_apigwv2_api(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
state.apis.remove(physical_id);
state.routes.remove(physical_id);
state.integrations.remove(physical_id);
state.stages.remove(physical_id);
state.deployments.remove(physical_id);
state.authorizers.remove(physical_id);
state.models.remove(physical_id);
state.integration_responses.remove(physical_id);
state.route_responses.remove(physical_id);
Ok(())
}
fn create_apigwv2_route(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let route_key = props
.get("RouteKey")
.and_then(|v| v.as_str())
.ok_or("RouteKey is required")?
.to_string();
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.apis.contains_key(&api_id) {
return Err(format!("Api {api_id} not yet provisioned"));
}
let id = make_apigwv2_id(10);
let route = ApiGwV2Route {
route_id: id.clone(),
route_key,
target: props
.get("Target")
.and_then(|v| v.as_str())
.map(String::from),
authorization_type: props
.get("AuthorizationType")
.and_then(|v| v.as_str())
.map(String::from),
authorizer_id: props
.get("AuthorizerId")
.and_then(|v| v.as_str())
.map(String::from),
};
state
.routes
.entry(api_id.clone())
.or_default()
.insert(id.clone(), route);
Ok(ProvisionResult::new(id.clone())
.with("RouteId", id)
.with("ApiId", api_id))
}
fn delete_apigwv2_route(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(api_id) = attributes.get("ApiId") else {
return Ok(());
};
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.routes.get_mut(api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigwv2_integration(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let integration_type = props
.get("IntegrationType")
.and_then(|v| v.as_str())
.ok_or("IntegrationType is required")?
.to_string();
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.apis.contains_key(&api_id) {
return Err(format!("Api {api_id} not yet provisioned"));
}
let id = make_apigwv2_id(10);
let integration = ApiGwV2Integration {
integration_id: id.clone(),
integration_type,
integration_uri: props
.get("IntegrationUri")
.and_then(|v| v.as_str())
.map(String::from),
payload_format_version: props
.get("PayloadFormatVersion")
.and_then(|v| v.as_str())
.map(String::from),
timeout_in_millis: props.get("TimeoutInMillis").and_then(|v| v.as_i64()),
};
state
.integrations
.entry(api_id.clone())
.or_default()
.insert(id.clone(), integration);
Ok(ProvisionResult::new(id.clone())
.with("IntegrationId", id)
.with("ApiId", api_id))
}
fn delete_apigwv2_integration(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(api_id) = attributes.get("ApiId") else {
return Ok(());
};
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.integrations.get_mut(api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigwv2_integration_response(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let integration_id = props
.get("IntegrationId")
.and_then(|v| v.as_str())
.ok_or("IntegrationId is required")?
.to_string();
let key_expr = props
.get("IntegrationResponseKey")
.and_then(|v| v.as_str())
.ok_or("IntegrationResponseKey is required")?
.to_string();
let id = make_apigwv2_id(10);
let body = serde_json::json!({
"integrationResponseId": id,
"integrationId": integration_id,
"integrationResponseKey": key_expr,
"responseTemplates": props.get("ResponseTemplates").cloned().unwrap_or(serde_json::json!({})),
"responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
"templateSelectionExpression": props.get("TemplateSelectionExpression").and_then(|v| v.as_str()),
"contentHandlingStrategy": props.get("ContentHandlingStrategy").and_then(|v| v.as_str()),
});
let composite_key = format!("{integration_id}/{id}");
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state
.integrations
.get(&api_id)
.map(|m| m.contains_key(&integration_id))
.unwrap_or(false)
{
return Err(format!(
"Integration {integration_id} not yet provisioned for api {api_id}"
));
}
state
.integration_responses
.entry(api_id.clone())
.or_default()
.insert(composite_key.clone(), body);
Ok(ProvisionResult::new(composite_key.clone())
.with("IntegrationResponseId", id)
.with("IntegrationId", integration_id)
.with("ApiId", api_id))
}
fn delete_apigwv2_integration_response(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(api_id) = attributes.get("ApiId") else {
return Ok(());
};
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.integration_responses.get_mut(api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigwv2_route_response(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let route_id = props
.get("RouteId")
.and_then(|v| v.as_str())
.ok_or("RouteId is required")?
.to_string();
let key_expr = props
.get("RouteResponseKey")
.and_then(|v| v.as_str())
.ok_or("RouteResponseKey is required")?
.to_string();
let id = make_apigwv2_id(10);
let body = serde_json::json!({
"routeResponseId": id,
"routeId": route_id,
"routeResponseKey": key_expr,
"responseModels": props.get("ResponseModels").cloned().unwrap_or(serde_json::json!({})),
"modelSelectionExpression": props.get("ModelSelectionExpression").and_then(|v| v.as_str()),
"responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
});
let composite = format!("{route_id}/{id}");
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state
.routes
.get(&api_id)
.map(|m| m.contains_key(&route_id))
.unwrap_or(false)
{
return Err(format!(
"Route {route_id} not yet provisioned for api {api_id}"
));
}
state
.route_responses
.entry(api_id.clone())
.or_default()
.insert(composite.clone(), body);
Ok(ProvisionResult::new(composite.clone())
.with("RouteResponseId", id)
.with("RouteId", route_id)
.with("ApiId", api_id))
}
fn delete_apigwv2_route_response(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(api_id) = attributes.get("ApiId") else {
return Ok(());
};
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.route_responses.get_mut(api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigwv2_stage(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let stage_name = props
.get("StageName")
.and_then(|v| v.as_str())
.ok_or("StageName is required")?
.to_string();
let auto_deploy = props
.get("AutoDeploy")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let deployment_id = props
.get("DeploymentId")
.and_then(|v| v.as_str())
.map(String::from);
let stage_variables = props
.get("StageVariables")
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
});
let access_log_settings = props.get("AccessLogSettings").and_then(|v| {
let destination_arn = v.get("DestinationArn")?.as_str()?.to_string();
let format = v.get("Format").and_then(|f| f.as_str().map(String::from));
Some(fakecloud_apigatewayv2::AccessLogSettings {
destination_arn,
format,
})
});
let stage = ApiGwV2Stage {
stage_name: stage_name.clone(),
description: props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from),
deployment_id: deployment_id.clone(),
auto_deploy,
created_date: Utc::now(),
last_updated_date: None,
web_acl_arn: None,
stage_variables,
access_log_settings,
};
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.apis.contains_key(&api_id) {
return Err(format!("Api {api_id} not yet provisioned"));
}
if let Some(dep) = &deployment_id {
if !state
.deployments
.get(&api_id)
.map(|m| m.contains_key(dep))
.unwrap_or(false)
{
return Err(format!(
"Deployment {dep} not yet provisioned for api {api_id}"
));
}
}
state
.stages
.entry(api_id.clone())
.or_default()
.insert(stage_name.clone(), stage);
Ok(ProvisionResult::new(stage_name.clone())
.with("StageName", stage_name)
.with("ApiId", api_id))
}
fn delete_apigwv2_stage(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(api_id) = attributes.get("ApiId") else {
return Ok(());
};
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.stages.get_mut(api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigwv2_deployment(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let id = make_apigwv2_id(10);
let deployment = ApiGwV2Deployment {
deployment_id: id.clone(),
description: props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from),
created_date: Utc::now(),
auto_deployed: false,
};
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.apis.contains_key(&api_id) {
return Err(format!("Api {api_id} not yet provisioned"));
}
state
.deployments
.entry(api_id.clone())
.or_default()
.insert(id.clone(), deployment);
Ok(ProvisionResult::new(id.clone())
.with("DeploymentId", id)
.with("ApiId", api_id))
}
fn delete_apigwv2_deployment(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(api_id) = attributes.get("ApiId") else {
return Ok(());
};
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.deployments.get_mut(api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigwv2_authorizer(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let authorizer_type = props
.get("AuthorizerType")
.and_then(|v| v.as_str())
.unwrap_or("REQUEST")
.to_string();
let identity_source = props
.get("IdentitySource")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect::<Vec<String>>()
});
let jwt_configuration = props
.get("JwtConfiguration")
.and_then(|v| v.as_object())
.map(|obj| ApiGwV2JwtConfiguration {
audience: obj.get("Audience").and_then(|v| v.as_array()).map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}),
issuer: obj.get("Issuer").and_then(|v| v.as_str()).map(String::from),
});
let id = make_apigwv2_id(10);
let auth = ApiGwV2Authorizer {
authorizer_id: id.clone(),
name,
authorizer_type,
authorizer_uri: props
.get("AuthorizerUri")
.and_then(|v| v.as_str())
.map(String::from),
identity_source,
jwt_configuration,
};
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.apis.contains_key(&api_id) {
return Err(format!("Api {api_id} not yet provisioned"));
}
state
.authorizers
.entry(api_id.clone())
.or_default()
.insert(id.clone(), auth);
Ok(ProvisionResult::new(id.clone())
.with("AuthorizerId", id)
.with("ApiId", api_id))
}
fn delete_apigwv2_authorizer(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(api_id) = attributes.get("ApiId") else {
return Ok(());
};
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.authorizers.get_mut(api_id) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigwv2_domain_name(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let domain_name = props
.get("DomainName")
.and_then(|v| v.as_str())
.ok_or("DomainName is required")?
.to_string();
let body = serde_json::json!({
"domainName": domain_name,
"domainNameConfigurations": props.get("DomainNameConfigurations").cloned().unwrap_or(serde_json::json!([])),
"mutualTlsAuthentication": props.get("MutualTlsAuthentication").cloned(),
"apiMappingSelectionExpression": null,
});
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
state.domain_names.insert(domain_name.clone(), body);
Ok(ProvisionResult::new(domain_name.clone()).with("DomainName", domain_name))
}
fn delete_apigwv2_domain_name(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
state.domain_names.remove(physical_id);
state.api_mappings.remove(physical_id);
Ok(())
}
fn create_apigwv2_api_mapping(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let domain_name = props
.get("DomainName")
.and_then(|v| v.as_str())
.ok_or("DomainName is required")?
.to_string();
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let stage = props
.get("Stage")
.and_then(|v| v.as_str())
.ok_or("Stage is required")?
.to_string();
let api_mapping_key = props
.get("ApiMappingKey")
.and_then(|v| v.as_str())
.map(String::from);
let id = make_apigwv2_id(10);
let body = serde_json::json!({
"apiMappingId": id,
"apiId": api_id,
"stage": stage,
"apiMappingKey": api_mapping_key,
});
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.domain_names.contains_key(&domain_name) {
return Err(format!("DomainName {domain_name} not yet provisioned"));
}
if !state.apis.contains_key(&api_id) {
return Err(format!("Api {api_id} not yet provisioned"));
}
state
.api_mappings
.entry(domain_name.clone())
.or_default()
.insert(id.clone(), body);
Ok(ProvisionResult::new(id.clone())
.with("ApiMappingId", id)
.with("DomainName", domain_name))
}
fn delete_apigwv2_api_mapping(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(domain) = attributes.get("DomainName") else {
return Ok(());
};
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.api_mappings.get_mut(domain) {
map.remove(physical_id);
}
Ok(())
}
fn create_apigwv2_vpc_link(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let id = make_apigwv2_id(10);
let body = serde_json::json!({
"vpcLinkId": id,
"name": name,
"subnetIds": props.get("SubnetIds").cloned().unwrap_or(serde_json::json!([])),
"securityGroupIds": props.get("SecurityGroupIds").cloned().unwrap_or(serde_json::json!([])),
"tags": props.get("Tags").cloned().unwrap_or(serde_json::json!({})),
"vpcLinkStatus": "AVAILABLE",
"vpcLinkVersion": "V2",
"createdDate": Utc::now().to_rfc3339(),
});
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
state.vpc_links.insert(id.clone(), body);
Ok(ProvisionResult::new(id.clone()).with("VpcLinkId", id))
}
fn delete_apigwv2_vpc_link(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
state.vpc_links.remove(physical_id);
Ok(())
}
fn create_apigwv2_model(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let id = make_apigwv2_id(10);
let body = serde_json::json!({
"modelId": id,
"name": name,
"contentType": props.get("ContentType").and_then(|v| v.as_str()).unwrap_or("application/json"),
"description": props.get("Description").and_then(|v| v.as_str()),
"schema": props.get("Schema").map(|v| if let Some(s) = v.as_str() { s.to_string() } else { v.to_string() }),
});
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.apis.contains_key(&api_id) {
return Err(format!("Api {api_id} not yet provisioned"));
}
state
.models
.entry(api_id.clone())
.or_default()
.insert(id.clone(), body);
Ok(ProvisionResult::new(id.clone())
.with("ModelId", id)
.with("ApiId", api_id))
}
fn delete_apigwv2_model(
&self,
physical_id: &str,
attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let Some(api_id) = attributes.get("ApiId") else {
return Ok(());
};
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(map) = state.models.get_mut(api_id) {
map.remove(physical_id);
}
Ok(())
}
fn update_apigwv2_api(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = existing.physical_id.clone();
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let api = state
.apis
.get_mut(&api_id)
.ok_or_else(|| format!("Api {api_id} no longer exists in state"))?;
if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
api.name = s.to_string();
}
if let Some(s) = props.get("ProtocolType").and_then(|v| v.as_str()) {
api.protocol_type = s.to_string();
}
api.description = props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from)
.or_else(|| api.description.clone());
if let Some(s) = props
.get("RouteSelectionExpression")
.and_then(|v| v.as_str())
{
api.route_selection_expression = s.to_string();
}
if let Some(s) = props
.get("ApiKeySelectionExpression")
.and_then(|v| v.as_str())
{
api.api_key_selection_expression = s.to_string();
}
if let Some(b) = props
.get("DisableExecuteApiEndpoint")
.and_then(|v| v.as_bool())
{
api.disable_execute_api_endpoint = b;
}
if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
api.ip_address_type = s.to_string();
}
if let Some(cors) = props.get("CorsConfiguration").and_then(|v| v.as_object()) {
api.cors_configuration = Some(ApiGwV2CorsConfiguration {
allow_credentials: cors.get("AllowCredentials").and_then(|v| v.as_bool()),
allow_headers: cors
.get("AllowHeaders")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}),
allow_methods: cors
.get("AllowMethods")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}),
allow_origins: cors
.get("AllowOrigins")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}),
expose_headers: cors
.get("ExposeHeaders")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}),
max_age: cors
.get("MaxAge")
.and_then(|v| v.as_i64())
.map(|n| n as i32),
});
}
if let Some(obj) = props.get("Tags").and_then(|v| v.as_object()) {
let tags: BTreeMap<String, String> = obj
.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect();
api.tags = Some(tags);
}
let api_endpoint = api.api_endpoint.clone();
Ok(ProvisionResult::new(api_id.clone())
.with("ApiId", api_id)
.with("ApiEndpoint", api_endpoint))
}
fn update_apigwv2_route(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let route_id = existing.physical_id.clone();
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let routes = state
.routes
.get_mut(&api_id)
.ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
let route = routes
.get_mut(&route_id)
.ok_or_else(|| format!("Route {route_id} not yet provisioned for api {api_id}"))?;
if let Some(s) = props.get("RouteKey").and_then(|v| v.as_str()) {
route.route_key = s.to_string();
}
if let Some(s) = props.get("Target").and_then(|v| v.as_str()) {
route.target = Some(s.to_string());
}
if let Some(s) = props.get("AuthorizationType").and_then(|v| v.as_str()) {
route.authorization_type = Some(s.to_string());
}
if let Some(s) = props.get("AuthorizerId").and_then(|v| v.as_str()) {
route.authorizer_id = Some(s.to_string());
}
Ok(ProvisionResult::new(route_id.clone())
.with("RouteId", route_id)
.with("ApiId", api_id))
}
fn update_apigwv2_integration(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let integration_id = existing.physical_id.clone();
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let integrations = state
.integrations
.get_mut(&api_id)
.ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
let integ = integrations.get_mut(&integration_id).ok_or_else(|| {
format!("Integration {integration_id} not yet provisioned for api {api_id}")
})?;
if let Some(s) = props.get("IntegrationType").and_then(|v| v.as_str()) {
integ.integration_type = s.to_string();
}
if let Some(s) = props.get("IntegrationUri").and_then(|v| v.as_str()) {
integ.integration_uri = Some(s.to_string());
}
if let Some(s) = props.get("PayloadFormatVersion").and_then(|v| v.as_str()) {
integ.payload_format_version = Some(s.to_string());
}
if let Some(n) = props.get("TimeoutInMillis").and_then(|v| v.as_i64()) {
integ.timeout_in_millis = Some(n);
}
Ok(ProvisionResult::new(integration_id.clone())
.with("IntegrationId", integration_id)
.with("ApiId", api_id))
}
fn update_apigwv2_integration_response(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let composite_key = existing.physical_id.clone();
let (integration_id, response_id) = composite_key
.split_once('/')
.map(|(a, b)| (a.to_string(), b.to_string()))
.ok_or_else(|| format!("Invalid IntegrationResponse physical id: {composite_key}"))?;
let key_expr = props
.get("IntegrationResponseKey")
.and_then(|v| v.as_str())
.ok_or("IntegrationResponseKey is required")?
.to_string();
let body = serde_json::json!({
"integrationResponseId": response_id,
"integrationId": integration_id,
"integrationResponseKey": key_expr,
"responseTemplates": props.get("ResponseTemplates").cloned().unwrap_or(serde_json::json!({})),
"responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
"templateSelectionExpression": props.get("TemplateSelectionExpression").and_then(|v| v.as_str()),
"contentHandlingStrategy": props.get("ContentHandlingStrategy").and_then(|v| v.as_str()),
});
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let map = state
.integration_responses
.get_mut(&api_id)
.ok_or_else(|| format!("No integration responses found for api {api_id}"))?;
if !map.contains_key(&composite_key) {
return Err(format!(
"IntegrationResponse {composite_key} not yet provisioned for api {api_id}"
));
}
map.insert(composite_key.clone(), body);
Ok(ProvisionResult::new(composite_key)
.with("IntegrationResponseId", response_id)
.with("IntegrationId", integration_id)
.with("ApiId", api_id))
}
fn update_apigwv2_route_response(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let composite = existing.physical_id.clone();
let (route_id, response_id) = composite
.split_once('/')
.map(|(a, b)| (a.to_string(), b.to_string()))
.ok_or_else(|| format!("Invalid RouteResponse physical id: {composite}"))?;
let key_expr = props
.get("RouteResponseKey")
.and_then(|v| v.as_str())
.ok_or("RouteResponseKey is required")?
.to_string();
let body = serde_json::json!({
"routeResponseId": response_id,
"routeId": route_id,
"routeResponseKey": key_expr,
"responseModels": props.get("ResponseModels").cloned().unwrap_or(serde_json::json!({})),
"modelSelectionExpression": props.get("ModelSelectionExpression").and_then(|v| v.as_str()),
"responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
});
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let map = state
.route_responses
.get_mut(&api_id)
.ok_or_else(|| format!("No route responses found for api {api_id}"))?;
if !map.contains_key(&composite) {
return Err(format!(
"RouteResponse {composite} not yet provisioned for api {api_id}"
));
}
map.insert(composite.clone(), body);
Ok(ProvisionResult::new(composite)
.with("RouteResponseId", response_id)
.with("RouteId", route_id)
.with("ApiId", api_id))
}
fn update_apigwv2_stage(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let stage_name = existing.physical_id.clone();
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let stages = state
.stages
.get_mut(&api_id)
.ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
let stage = stages
.get_mut(&stage_name)
.ok_or_else(|| format!("Stage {stage_name} not yet provisioned for api {api_id}"))?;
if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
stage.description = Some(s.to_string());
}
if let Some(s) = props.get("DeploymentId").and_then(|v| v.as_str()) {
stage.deployment_id = Some(s.to_string());
}
if let Some(b) = props.get("AutoDeploy").and_then(|v| v.as_bool()) {
stage.auto_deploy = b;
}
stage.last_updated_date = Some(Utc::now());
Ok(ProvisionResult::new(stage_name.clone())
.with("StageName", stage_name)
.with("ApiId", api_id))
}
fn update_apigwv2_deployment(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let deployment_id = existing.physical_id.clone();
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let deps = state
.deployments
.get_mut(&api_id)
.ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
let dep = deps.get_mut(&deployment_id).ok_or_else(|| {
format!("Deployment {deployment_id} not yet provisioned for api {api_id}")
})?;
if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
dep.description = Some(s.to_string());
}
Ok(ProvisionResult::new(deployment_id.clone())
.with("DeploymentId", deployment_id)
.with("ApiId", api_id))
}
fn update_apigwv2_authorizer(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let authorizer_id = existing.physical_id.clone();
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let auths = state
.authorizers
.get_mut(&api_id)
.ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
let auth = auths.get_mut(&authorizer_id).ok_or_else(|| {
format!("Authorizer {authorizer_id} not yet provisioned for api {api_id}")
})?;
if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
auth.name = s.to_string();
}
if let Some(s) = props.get("AuthorizerType").and_then(|v| v.as_str()) {
auth.authorizer_type = s.to_string();
}
if let Some(s) = props.get("AuthorizerUri").and_then(|v| v.as_str()) {
auth.authorizer_uri = Some(s.to_string());
}
if let Some(arr) = props.get("IdentitySource").and_then(|v| v.as_array()) {
auth.identity_source = Some(
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
);
}
if let Some(obj) = props.get("JwtConfiguration").and_then(|v| v.as_object()) {
auth.jwt_configuration = Some(ApiGwV2JwtConfiguration {
audience: obj.get("Audience").and_then(|v| v.as_array()).map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}),
issuer: obj.get("Issuer").and_then(|v| v.as_str()).map(String::from),
});
}
Ok(ProvisionResult::new(authorizer_id.clone())
.with("AuthorizerId", authorizer_id)
.with("ApiId", api_id))
}
fn update_apigwv2_domain_name(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let domain_name = existing.physical_id.clone();
let body = serde_json::json!({
"domainName": domain_name,
"domainNameConfigurations": props.get("DomainNameConfigurations").cloned().unwrap_or(serde_json::json!([])),
"mutualTlsAuthentication": props.get("MutualTlsAuthentication").cloned(),
"apiMappingSelectionExpression": null,
});
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.domain_names.contains_key(&domain_name) {
return Err(format!("DomainName {domain_name} no longer exists"));
}
state.domain_names.insert(domain_name.clone(), body);
Ok(ProvisionResult::new(domain_name.clone()).with("DomainName", domain_name))
}
fn update_apigwv2_api_mapping(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let domain_name = props
.get("DomainName")
.and_then(|v| v.as_str())
.ok_or("DomainName is required")?
.to_string();
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let stage = props
.get("Stage")
.and_then(|v| v.as_str())
.ok_or("Stage is required")?
.to_string();
let api_mapping_key = props
.get("ApiMappingKey")
.and_then(|v| v.as_str())
.map(String::from);
let id = existing.physical_id.clone();
let body = serde_json::json!({
"apiMappingId": id,
"apiId": api_id,
"stage": stage,
"apiMappingKey": api_mapping_key,
});
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let map = state
.api_mappings
.get_mut(&domain_name)
.ok_or_else(|| format!("DomainName {domain_name} no longer exists"))?;
map.insert(id.clone(), body);
Ok(ProvisionResult::new(id.clone())
.with("ApiMappingId", id)
.with("DomainName", domain_name))
}
fn update_apigwv2_vpc_link(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let id = existing.physical_id.clone();
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let body = state
.vpc_links
.get_mut(&id)
.ok_or_else(|| format!("VpcLink {id} no longer exists"))?;
if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
body["name"] = serde_json::Value::String(s.to_string());
}
if let Some(v) = props.get("SubnetIds").cloned() {
body["subnetIds"] = v;
}
if let Some(v) = props.get("SecurityGroupIds").cloned() {
body["securityGroupIds"] = v;
}
if let Some(v) = props.get("Tags").cloned() {
body["tags"] = v;
}
Ok(ProvisionResult::new(id.clone()).with("VpcLinkId", id))
}
fn update_apigwv2_model(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let api_id = props
.get("ApiId")
.and_then(|v| v.as_str())
.ok_or("ApiId is required")?
.to_string();
let id = existing.physical_id.clone();
let mut accounts = self.apigatewayv2_state.write();
let state = accounts.get_or_create(&self.account_id);
let map = state
.models
.get_mut(&api_id)
.ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
let body = map
.get_mut(&id)
.ok_or_else(|| format!("Model {id} not yet provisioned for api {api_id}"))?;
if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
body["name"] = serde_json::Value::String(s.to_string());
}
if let Some(s) = props.get("ContentType").and_then(|v| v.as_str()) {
body["contentType"] = serde_json::Value::String(s.to_string());
}
if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
body["description"] = serde_json::Value::String(s.to_string());
}
if let Some(v) = props.get("Schema") {
body["schema"] = serde_json::Value::String(if let Some(s) = v.as_str() {
s.to_string()
} else {
v.to_string()
});
}
Ok(ProvisionResult::new(id.clone())
.with("ModelId", id)
.with("ApiId", api_id))
}
fn create_ses_configuration_set(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| format!("cfn-cs-{}", resource.logical_id));
let sending_enabled = props
.get("SendingOptions")
.and_then(|v| v.get("SendingEnabled"))
.and_then(|v| v.as_bool())
.unwrap_or(true);
let tls_policy = props
.get("DeliveryOptions")
.and_then(|v| v.get("TlsPolicy"))
.and_then(|v| v.as_str())
.unwrap_or("OPTIONAL")
.to_string();
let sending_pool_name = props
.get("DeliveryOptions")
.and_then(|v| v.get("SendingPoolName"))
.and_then(|v| v.as_str())
.map(String::from);
let custom_redirect_domain = props
.get("TrackingOptions")
.and_then(|v| v.get("CustomRedirectDomain"))
.and_then(|v| v.as_str())
.map(String::from);
let suppressed_reasons: Vec<String> = props
.get("SuppressionOptions")
.and_then(|v| v.get("SuppressedReasons"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let reputation_metrics_enabled = props
.get("ReputationOptions")
.and_then(|v| v.get("ReputationMetricsEnabled"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let cs = SesConfigurationSet {
name: name.clone(),
sending_enabled,
tls_policy,
sending_pool_name,
custom_redirect_domain,
https_policy: props
.get("TrackingOptions")
.and_then(|v| v.get("HttpsPolicy"))
.and_then(|v| v.as_str())
.map(String::from),
suppressed_reasons,
reputation_metrics_enabled,
vdm_options: props.get("VdmOptions").cloned(),
archive_arn: props
.get("ArchivingOptions")
.and_then(|v| v.get("ArchiveArn"))
.and_then(|v| v.as_str())
.map(String::from),
};
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.configuration_sets.insert(name.clone(), cs);
Ok(ProvisionResult::new(name.clone()).with("Name", name))
}
fn delete_ses_configuration_set(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.configuration_sets.remove(physical_id);
state.event_destinations.remove(physical_id);
Ok(())
}
fn create_ses_event_destination(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let cs_name = props
.get("ConfigurationSetName")
.and_then(|v| v.as_str())
.ok_or("ConfigurationSetName is required")?
.to_string();
let dest_props = props
.get("EventDestination")
.and_then(|v| v.as_object())
.ok_or("EventDestination is required")?;
let name = dest_props
.get("Name")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| format!("cfn-ed-{}", resource.logical_id));
let enabled = dest_props
.get("Enabled")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let matching_event_types: Vec<String> = dest_props
.get("MatchingEventTypes")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let dest = SesEventDestination {
name: name.clone(),
enabled,
matching_event_types,
kinesis_firehose_destination: dest_props.get("KinesisFirehoseDestination").cloned(),
cloud_watch_destination: dest_props.get("CloudWatchDestination").cloned(),
sns_destination: dest_props.get("SnsDestination").cloned(),
event_bridge_destination: dest_props.get("EventBridgeDestination").cloned(),
pinpoint_destination: dest_props.get("PinpointDestination").cloned(),
};
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
if !state.configuration_sets.contains_key(&cs_name) {
return Err(format!("ConfigurationSet {cs_name} not yet provisioned"));
}
let dests = state.event_destinations.entry(cs_name.clone()).or_default();
dests.retain(|d| d.name != name);
dests.push(dest);
let physical = format!("{cs_name}|{name}");
Ok(ProvisionResult::new(physical)
.with("Name", name)
.with("ConfigurationSetName", cs_name))
}
fn delete_ses_event_destination(
&self,
physical_id: &str,
_attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let mut parts = physical_id.splitn(2, '|');
let Some(cs) = parts.next() else {
return Ok(());
};
let Some(name) = parts.next() else {
return Ok(());
};
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(dests) = state.event_destinations.get_mut(cs) {
dests.retain(|d| d.name != name);
}
Ok(())
}
fn create_ses_email_identity(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let identity_name = props
.get("EmailIdentity")
.and_then(|v| v.as_str())
.ok_or("EmailIdentity is required")?
.to_string();
let identity_type = if identity_name.contains('@') {
"EMAIL_ADDRESS"
} else {
"DOMAIN"
}
.to_string();
let dkim_signing_enabled = props
.get("DkimAttributes")
.and_then(|v| v.get("SigningEnabled"))
.and_then(|v| v.as_bool())
.unwrap_or(true);
let dkim_signing_attributes_origin = props
.get("DkimSigningAttributes")
.map(|_| "EXTERNAL")
.unwrap_or("AWS_SES")
.to_string();
let mail_from_domain = props
.get("MailFromAttributes")
.and_then(|v| v.get("MailFromDomain"))
.and_then(|v| v.as_str())
.map(String::from);
let mail_from_behavior = props
.get("MailFromAttributes")
.and_then(|v| v.get("BehaviorOnMxFailure"))
.and_then(|v| v.as_str())
.unwrap_or("USE_DEFAULT_VALUE")
.to_string();
let configuration_set_name = props
.get("ConfigurationSetAttributes")
.and_then(|v| v.get("ConfigurationSetName"))
.and_then(|v| v.as_str())
.map(String::from);
let email_forwarding_enabled = props
.get("FeedbackAttributes")
.and_then(|v| v.get("EmailForwardingEnabled"))
.and_then(|v| v.as_bool())
.unwrap_or(true);
let identity = SesEmailIdentity {
identity_name: identity_name.clone(),
identity_type,
verified: true,
created_at: Utc::now(),
dkim_signing_enabled,
dkim_signing_attributes_origin,
dkim_domain_signing_private_key: None,
dkim_domain_signing_selector: None,
dkim_next_signing_key_length: None,
dkim_public_key_b64: None,
email_forwarding_enabled,
mail_from_domain,
mail_from_behavior_on_mx_failure: mail_from_behavior,
mail_from_domain_status: "Success".to_string(),
configuration_set_name,
};
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.identities.insert(identity_name.clone(), identity);
Ok(ProvisionResult::new(identity_name.clone()).with("EmailIdentity", identity_name))
}
fn delete_ses_email_identity(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.identities.remove(physical_id);
Ok(())
}
fn create_ses_template(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let template_block = props
.get("Template")
.and_then(|v| v.as_object())
.ok_or("Template is required")?;
let template_name = template_block
.get("TemplateName")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| format!("cfn-tpl-{}", resource.logical_id));
let tpl = SesEmailTemplate {
template_name: template_name.clone(),
subject: template_block
.get("SubjectPart")
.and_then(|v| v.as_str())
.map(String::from),
html_body: template_block
.get("HtmlPart")
.and_then(|v| v.as_str())
.map(String::from),
text_body: template_block
.get("TextPart")
.and_then(|v| v.as_str())
.map(String::from),
created_at: Utc::now(),
};
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.templates.insert(template_name.clone(), tpl);
Ok(ProvisionResult::new(template_name.clone()).with("TemplateName", template_name))
}
fn delete_ses_template(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.templates.remove(physical_id);
Ok(())
}
fn create_ses_contact_list(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("ContactListName")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| format!("cfn-cl-{}", resource.logical_id));
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(String::from);
let now = Utc::now();
let cl = SesContactList {
contact_list_name: name.clone(),
description,
topics: Vec::new(),
created_at: now,
last_updated_at: now,
};
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.contact_lists.insert(name.clone(), cl);
Ok(ProvisionResult::new(name.clone()).with("ContactListName", name))
}
fn delete_ses_contact_list(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.contact_lists.remove(physical_id);
state.contacts.remove(physical_id);
Ok(())
}
fn create_ses_dedicated_ip_pool(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("PoolName")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| format!("cfn-pool-{}", resource.logical_id));
let scaling_mode = props
.get("ScalingMode")
.and_then(|v| v.as_str())
.unwrap_or("STANDARD")
.to_string();
let pool = SesDedicatedIpPool {
pool_name: name.clone(),
scaling_mode,
};
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.dedicated_ip_pools.insert(name.clone(), pool);
Ok(ProvisionResult::new(name.clone()).with("PoolName", name))
}
fn delete_ses_dedicated_ip_pool(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.dedicated_ip_pools.remove(physical_id);
Ok(())
}
fn create_ses_receipt_rule_set(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("RuleSetName")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| format!("cfn-rs-{}", resource.logical_id));
let rs = SesReceiptRuleSet {
name: name.clone(),
rules: Vec::new(),
created_at: Utc::now(),
};
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.receipt_rule_sets.insert(name.clone(), rs);
Ok(ProvisionResult::new(name.clone()).with("RuleSetName", name))
}
fn delete_ses_receipt_rule_set(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.receipt_rule_sets.remove(physical_id);
Ok(())
}
fn create_ses_receipt_rule(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let rule_set_name = props
.get("RuleSetName")
.and_then(|v| v.as_str())
.ok_or("RuleSetName is required")?
.to_string();
let rule_block = props
.get("Rule")
.and_then(|v| v.as_object())
.ok_or("Rule is required")?;
let name = rule_block
.get("Name")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| format!("cfn-rule-{}", resource.logical_id));
let enabled = rule_block
.get("Enabled")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let scan_enabled = rule_block
.get("ScanEnabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let tls_policy = rule_block
.get("TlsPolicy")
.and_then(|v| v.as_str())
.unwrap_or("Optional")
.to_string();
let recipients: Vec<String> = rule_block
.get("Recipients")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let actions: Vec<SesReceiptAction> = rule_block
.get("Actions")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(parse_ses_receipt_action).collect())
.unwrap_or_default();
let rule = SesReceiptRule {
name: name.clone(),
enabled,
scan_enabled,
tls_policy,
recipients,
actions,
};
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
let rs = state
.receipt_rule_sets
.get_mut(&rule_set_name)
.ok_or_else(|| format!("ReceiptRuleSet {rule_set_name} not yet provisioned"))?;
rs.rules.retain(|r| r.name != name);
rs.rules.push(rule);
let physical = format!("{rule_set_name}|{name}");
Ok(ProvisionResult::new(physical)
.with("Name", name)
.with("RuleSetName", rule_set_name))
}
fn delete_ses_receipt_rule(
&self,
physical_id: &str,
_attributes: &BTreeMap<String, String>,
) -> Result<(), String> {
let mut parts = physical_id.splitn(2, '|');
let Some(rs_name) = parts.next() else {
return Ok(());
};
let Some(rule_name) = parts.next() else {
return Ok(());
};
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(rs) = state.receipt_rule_sets.get_mut(rs_name) {
rs.rules.retain(|r| r.name != rule_name);
}
Ok(())
}
fn create_ses_receipt_filter(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let filter_block = props
.get("Filter")
.and_then(|v| v.as_object())
.ok_or("Filter is required")?;
let name = filter_block
.get("Name")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| format!("cfn-filter-{}", resource.logical_id));
let ip_block = filter_block
.get("IpFilter")
.and_then(|v| v.as_object())
.ok_or("Filter.IpFilter is required")?;
let cidr = ip_block
.get("Cidr")
.and_then(|v| v.as_str())
.ok_or("Filter.IpFilter.Cidr is required")?
.to_string();
let policy = ip_block
.get("Policy")
.and_then(|v| v.as_str())
.unwrap_or("Block")
.to_string();
let filter = SesReceiptFilter {
name: name.clone(),
ip_filter: SesIpFilter { cidr, policy },
};
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.receipt_filters.insert(name.clone(), filter);
Ok(ProvisionResult::new(name.clone()).with("Name", name))
}
fn delete_ses_receipt_filter(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.receipt_filters.remove(physical_id);
Ok(())
}
fn create_ses_vdm_attributes(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let mut accounts = self.ses_state.write();
let state = accounts.get_or_create(&self.account_id);
state.account_settings.vdm_attributes = Some(props.clone());
Ok(ProvisionResult::new(format!("vdm-{}", resource.logical_id)))
}
fn create_secrets_manager_rotation_schedule(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let secret_id = props
.get("SecretId")
.and_then(|v| v.as_str())
.ok_or("SecretId is required")?
.to_string();
let rotation_lambda_arn = props
.get("RotationLambdaARN")
.and_then(|v| v.as_str())
.map(String::from);
let automatically_after_days = props
.get("RotationRules")
.and_then(|v| v.get("AutomaticallyAfterDays"))
.and_then(|v| v.as_i64());
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
let secret_arn = if state.secrets.contains_key(&secret_id) {
secret_id.clone()
} else {
let candidate = format!(
"arn:aws:secretsmanager:{}:{}:secret:{}",
state.region, state.account_id, secret_id
);
if state.secrets.contains_key(&candidate) {
candidate
} else {
return Err(format!("Secret {secret_id} not yet provisioned"));
}
};
let secret = state
.secrets
.get_mut(&secret_arn)
.ok_or_else(|| format!("Secret {secret_arn} not found"))?;
secret.rotation_enabled = Some(true);
secret.rotation_lambda_arn = rotation_lambda_arn;
secret.rotation_rules = Some(RotationRules {
automatically_after_days,
});
secret.last_changed_at = Utc::now();
Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
}
fn delete_secrets_manager_rotation_schedule(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(secret) = state.secrets.get_mut(physical_id) {
secret.rotation_enabled = Some(false);
secret.rotation_lambda_arn = None;
secret.rotation_rules = None;
secret.last_changed_at = Utc::now();
}
Ok(())
}
fn create_secrets_manager_resource_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let secret_id = props
.get("SecretId")
.and_then(|v| v.as_str())
.ok_or("SecretId is required")?
.to_string();
let policy_doc = props
.get("ResourcePolicy")
.ok_or("ResourcePolicy is required")?;
let policy_str = match policy_doc {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
let secret_arn = if state.secrets.contains_key(&secret_id) {
secret_id.clone()
} else {
let candidate = format!(
"arn:aws:secretsmanager:{}:{}:secret:{}",
state.region, state.account_id, secret_id
);
if state.secrets.contains_key(&candidate) {
candidate
} else {
return Err(format!("Secret {secret_id} not yet provisioned"));
}
};
let secret = state
.secrets
.get_mut(&secret_arn)
.ok_or_else(|| format!("Secret {secret_arn} not found"))?;
secret.resource_policy = Some(policy_str);
secret.last_changed_at = Utc::now();
Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
}
fn delete_secrets_manager_resource_policy(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(secret) = state.secrets.get_mut(physical_id) {
secret.resource_policy = None;
secret.last_changed_at = Utc::now();
}
Ok(())
}
fn create_secrets_manager_target_attachment(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let secret_id = props
.get("SecretId")
.and_then(|v| v.as_str())
.ok_or("SecretId is required")?
.to_string();
let target_type = props
.get("TargetType")
.and_then(|v| v.as_str())
.ok_or("TargetType is required")?;
let target_id = props
.get("TargetId")
.and_then(|v| v.as_str())
.ok_or("TargetId is required")?;
let mut accounts = self.secretsmanager_state.write();
let state = accounts.get_or_create(&self.account_id);
let secret_arn = if state.secrets.contains_key(&secret_id) {
secret_id.clone()
} else {
let candidate = format!(
"arn:aws:secretsmanager:{}:{}:secret:{}",
state.region, state.account_id, secret_id
);
if state.secrets.contains_key(&candidate) {
candidate
} else {
return Err(format!("Secret {secret_id} not yet provisioned"));
}
};
let secret = state
.secrets
.get_mut(&secret_arn)
.ok_or_else(|| format!("Secret {secret_arn} not found"))?;
let now = Utc::now();
if secret.current_version_id.is_none() {
let version_id = Uuid::new_v4().to_string();
secret.versions.insert(
version_id.clone(),
SecretVersion {
version_id: version_id.clone(),
secret_string: Some("{}".to_string()),
secret_binary: None,
stages: vec!["AWSCURRENT".to_string()],
created_at: now,
},
);
secret.current_version_id = Some(version_id);
}
if let Some(version_id) = secret.current_version_id.clone() {
if let Some(version) = secret.versions.get_mut(&version_id) {
let mut existing: serde_json::Value = version
.secret_string
.as_deref()
.and_then(|s| serde_json::from_str(s).ok())
.unwrap_or_else(|| serde_json::json!({}));
if let Some(obj) = existing.as_object_mut() {
let engine = match target_type {
"AWS::RDS::DBInstance" | "AWS::RDS::DBCluster" => "postgres",
_ => "unknown",
};
obj.entry("engine".to_string())
.or_insert(serde_json::json!(engine));
obj.insert("host".to_string(), serde_json::json!(target_id));
obj.entry("dbInstanceIdentifier".to_string())
.or_insert(serde_json::json!(target_id));
}
version.secret_string = Some(existing.to_string());
}
}
secret.last_changed_at = now;
Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
}
fn create_athena_work_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let configuration = props.get("Configuration").cloned();
let state_str = props
.get("State")
.and_then(|v| v.as_str())
.unwrap_or("ENABLED");
let tags = Self::parse_athena_tags(props.get("Tags"));
let mut accounts = self.athena_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
account.ensure_initialized();
if account.work_groups.contains_key(&name) {
return Err(format!("WorkGroup {name} already exists"));
}
let wg = WorkGroup {
name: name.clone(),
state: state_str.to_string(),
description,
configuration,
creation_time: Utc::now(),
engine_version: Some("AUTO".to_string()),
};
let arn = format!(
"arn:aws:athena:{}:{}:workgroup/{}",
self.region, self.account_id, name
);
account.work_groups.insert(name.clone(), wg);
if !tags.is_empty() {
account.tags.insert(arn.clone(), tags);
}
Ok(ProvisionResult::new(name.clone())
.with("Arn", arn)
.with("Name", name))
}
fn delete_athena_work_group(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.athena_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
account.work_groups.remove(physical_id);
Ok(())
}
fn get_att_athena_work_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.athena_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
let wg = account.work_groups.get(physical_id)?;
match attribute {
"Arn" => Some(format!(
"arn:aws:athena:{}:{}:workgroup/{}",
self.region, self.account_id, wg.name
)),
"Name" => Some(wg.name.clone()),
_ => None,
}
}
fn create_athena_data_catalog(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let cat_type = props
.get("Type")
.and_then(|v| v.as_str())
.ok_or("Type is required")?
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let parameters = props
.get("Parameters")
.and_then(|v| v.as_object())
.map(|m| {
m.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let connection_type = props
.get("ConnectionType")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let tags = Self::parse_athena_tags(props.get("Tags"));
let mut accounts = self.athena_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
account.ensure_initialized();
if account.data_catalogs.contains_key(&name) {
return Err(format!("DataCatalog {name} already exists"));
}
let cat = DataCatalog {
name: name.clone(),
description,
cat_type,
parameters,
status: "CREATE_COMPLETE".to_string(),
connection_type,
error: None,
};
let arn = format!(
"arn:aws:athena:{}:{}:datacatalog/{}",
self.region, self.account_id, name
);
account.data_catalogs.insert(name.clone(), cat);
if !tags.is_empty() {
account.tags.insert(arn.clone(), tags);
}
Ok(ProvisionResult::new(name.clone())
.with("Arn", arn)
.with("Name", name))
}
fn delete_athena_data_catalog(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.athena_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
account.data_catalogs.remove(physical_id);
Ok(())
}
fn get_att_athena_data_catalog(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.athena_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
let cat = account.data_catalogs.get(physical_id)?;
match attribute {
"Arn" => Some(format!(
"arn:aws:athena:{}:{}:datacatalog/{}",
self.region, self.account_id, cat.name
)),
"Name" => Some(cat.name.clone()),
_ => None,
}
}
fn create_athena_named_query(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = props
.get("Name")
.and_then(|v| v.as_str())
.ok_or("Name is required")?
.to_string();
let database = props
.get("Database")
.and_then(|v| v.as_str())
.ok_or("Database is required")?
.to_string();
let query_string = props
.get("QueryString")
.and_then(|v| v.as_str())
.ok_or("QueryString is required")?
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let work_group = props
.get("WorkGroup")
.and_then(|v| v.as_str())
.unwrap_or("primary")
.to_string();
let mut accounts = self.athena_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
account.ensure_initialized();
if !account.work_groups.contains_key(&work_group) {
return Err(format!("Workgroup {work_group} not found"));
}
let id = Uuid::new_v4().to_string();
let nq = NamedQuery {
named_query_id: id.clone(),
name,
description,
database,
query_string,
work_group,
last_used_at: None,
};
account.named_queries.insert(id.clone(), nq);
Ok(ProvisionResult::new(id.clone()).with("NamedQueryId", id))
}
fn delete_athena_named_query(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.athena_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
account.named_queries.remove(physical_id);
Ok(())
}
fn get_att_athena_named_query(&self, physical_id: &str, attribute: &str) -> Option<String> {
let mut accounts = self.athena_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
let nq = account.named_queries.get(physical_id)?;
match attribute {
"NamedQueryId" => Some(nq.named_query_id.clone()),
_ => None,
}
}
fn create_athena_prepared_statement(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let statement_name = props
.get("StatementName")
.and_then(|v| v.as_str())
.ok_or("StatementName is required")?
.to_string();
let work_group_name = props
.get("WorkGroupName")
.and_then(|v| v.as_str())
.ok_or("WorkGroupName is required")?
.to_string();
let query_statement = props
.get("QueryStatement")
.and_then(|v| v.as_str())
.ok_or("QueryStatement is required")?
.to_string();
let description = props
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut accounts = self.athena_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
account.ensure_initialized();
if !account.work_groups.contains_key(&work_group_name) {
return Err(format!("Workgroup {work_group_name} not found"));
}
let key = (work_group_name.clone(), statement_name.clone());
if account.prepared_statements.contains_key(&key) {
return Err(format!(
"PreparedStatement {statement_name} already exists in {work_group_name}"
));
}
let ps = PreparedStatement {
statement_name: statement_name.clone(),
work_group_name: work_group_name.clone(),
query_statement,
description,
last_modified_time: Utc::now(),
};
let physical_id = format!("{work_group_name}|{statement_name}");
account.prepared_statements.insert(key, ps);
Ok(ProvisionResult::new(physical_id))
}
fn delete_athena_prepared_statement(
&self,
physical_id: &str,
_attrs: &BTreeMap<String, String>,
) -> Result<(), String> {
let mut accounts = self.athena_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
let parts: Vec<&str> = physical_id.split('|').collect();
if parts.len() != 2 {
return Err(format!(
"Invalid PreparedStatement physical id: {physical_id}"
));
}
let key = (parts[0].to_string(), parts[1].to_string());
account.prepared_statements.remove(&key);
Ok(())
}
fn get_att_athena_prepared_statement(
&self,
physical_id: &str,
attribute: &str,
) -> Option<String> {
let mut accounts = self.athena_state.write();
let account = accounts
.accounts
.entry(self.account_id.clone())
.or_default();
let parts: Vec<&str> = physical_id.split('|').collect();
if parts.len() != 2 {
return None;
}
let ps = account
.prepared_statements
.get(&(parts[0].to_string(), parts[1].to_string()))?;
match attribute {
"StatementName" => Some(ps.statement_name.clone()),
"WorkGroupName" => Some(ps.work_group_name.clone()),
_ => None,
}
}
fn parse_athena_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
let Some(arr) = value.and_then(|v| v.as_array()) else {
return out;
};
for tag in arr {
if let (Some(k), Some(v)) = (
tag.get("Key").and_then(|v| v.as_str()),
tag.get("Value").and_then(|v| v.as_str()),
) {
out.insert(k.to_string(), v.to_string());
}
}
out
}
fn create_glue_database(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let input = props
.get("DatabaseInput")
.ok_or("DatabaseInput is required")?;
let name = input
.get("Name")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let description = input
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let location_uri = input
.get("LocationUri")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let parameters = input
.get("Parameters")
.and_then(|v| v.as_object())
.map(|m| {
m.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let mut accounts = self.glue_state.write();
let state = accounts.get_or_create(&self.account_id, &self.region);
let dbs = state.dbs_in_mut(&self.region);
if dbs.contains_key(&name) {
return Err(format!("Database {name} already exists"));
}
dbs.insert(
name.clone(),
fakecloud_glue::Database {
name: name.clone(),
description,
location_uri,
parameters,
created_at: Utc::now(),
catalog_id: self.account_id.clone(),
tables: BTreeMap::new(),
},
);
Ok(ProvisionResult::new(name.clone()))
}
fn delete_glue_database(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.glue_state.write();
let state = accounts.get_or_create(&self.account_id, &self.region);
state.dbs_in_mut(&self.region).remove(physical_id);
Ok(())
}
fn create_glue_table(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let db_name = props
.get("DatabaseName")
.and_then(|v| v.as_str())
.ok_or("DatabaseName is required")?
.to_string();
let input = props.get("TableInput").ok_or("TableInput is required")?;
let name = input
.get("Name")
.and_then(|v| v.as_str())
.ok_or("TableInput.Name is required")?
.to_string();
let now = Utc::now();
let mut accounts = self.glue_state.write();
let state = accounts.get_or_create(&self.account_id, &self.region);
let dbs = state.dbs_in_mut(&self.region);
let db = dbs
.get_mut(&db_name)
.ok_or_else(|| format!("Database {db_name} not found"))?;
if db.tables.contains_key(&name) {
return Err(format!("Table {name} already exists"));
}
db.tables.insert(
name.clone(),
fakecloud_glue::Table {
name: name.clone(),
database_name: db_name.clone(),
description: input
.get("Description")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
owner: input
.get("Owner")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
create_time: now,
update_time: now,
last_access_time: None,
retention: input.get("Retention").and_then(|v| v.as_i64()).unwrap_or(0),
storage_descriptor: None,
partition_keys: Vec::new(),
view_original_text: input
.get("ViewOriginalText")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
view_expanded_text: input
.get("ViewExpandedText")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
table_type: input
.get("TableType")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
parameters: BTreeMap::new(),
partitions: BTreeMap::new(),
},
);
let physical_id = format!("{db_name}|{name}");
Ok(ProvisionResult::new(physical_id))
}
fn delete_glue_table(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.glue_state.write();
let state = accounts.get_or_create(&self.account_id, &self.region);
let parts: Vec<&str> = physical_id.split('|').collect();
if parts.len() != 2 {
return Err(format!("Invalid Glue table physical id: {physical_id}"));
}
let dbs = state.dbs_in_mut(&self.region);
let db = dbs
.get_mut(parts[0])
.ok_or_else(|| format!("Database {} not found", parts[0]))?;
db.tables.remove(parts[1]);
Ok(())
}
fn create_glue_partition(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let db_name = props
.get("DatabaseName")
.and_then(|v| v.as_str())
.ok_or("DatabaseName is required")?
.to_string();
let table_name = props
.get("TableName")
.and_then(|v| v.as_str())
.ok_or("TableName is required")?
.to_string();
let input = props
.get("PartitionInput")
.ok_or("PartitionInput is required")?;
let values: Vec<String> = input
.get("Values")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
if values.is_empty() {
return Err("PartitionInput.Values is required".to_string());
}
let key = values
.iter()
.map(|v| {
v.replace('%', "%25")
.replace('|', "%7C")
.replace('/', "%2F")
})
.collect::<Vec<_>>()
.join("/");
let mut accounts = self.glue_state.write();
let state = accounts.get_or_create(&self.account_id, &self.region);
let dbs = state.dbs_in_mut(&self.region);
let db = dbs
.get_mut(&db_name)
.ok_or_else(|| format!("Database {db_name} not found"))?;
let table = db
.tables
.get_mut(&table_name)
.ok_or_else(|| format!("Table {table_name} not found"))?;
if table.partitions.contains_key(&key) {
return Err(format!("Partition {key} already exists"));
}
table.partitions.insert(
key.clone(),
fakecloud_glue::Partition {
values: values.clone(),
database_name: db_name.clone(),
table_name: table_name.clone(),
create_time: Utc::now(),
last_access_time: None,
storage_descriptor: None,
parameters: BTreeMap::new(),
},
);
let physical_id = format!("{db_name}|{table_name}|{key}");
Ok(ProvisionResult::new(physical_id))
}
fn delete_glue_partition(
&self,
physical_id: &str,
_attrs: &BTreeMap<String, String>,
) -> Result<(), String> {
let mut accounts = self.glue_state.write();
let state = accounts.get_or_create(&self.account_id, &self.region);
let parts: Vec<&str> = physical_id.split('|').collect();
if parts.len() != 3 {
return Err(format!("Invalid Glue partition physical id: {physical_id}"));
}
let dbs = state.dbs_in_mut(&self.region);
let db = dbs
.get_mut(parts[0])
.ok_or_else(|| format!("Database {} not found", parts[0]))?;
let table = db
.tables
.get_mut(parts[1])
.ok_or_else(|| format!("Table {} not found", parts[1]))?;
table.partitions.remove(parts[2]);
Ok(())
}
fn create_cloudformation_stack(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let template_body = if let Some(body) = props.get("TemplateBody").and_then(|v| v.as_str()) {
body.to_string()
} else if let Some(url) = props.get("TemplateURL").and_then(|v| v.as_str()) {
self.fetch_template_from_url(url)?
} else {
return Err(
"AWS::CloudFormation::Stack requires TemplateURL or TemplateBody".to_string(),
);
};
let child_parameters =
if let Some(params) = props.get("Parameters").and_then(|v| v.as_object()) {
let mut map = BTreeMap::new();
for (k, v) in params {
if let Some(s) = v.as_str() {
map.insert(k.clone(), s.to_string());
} else {
map.insert(k.clone(), v.to_string());
}
}
map
} else {
BTreeMap::new()
};
let child_tags = if let Some(tags) = props.get("Tags").and_then(|v| v.as_object()) {
let mut map = BTreeMap::new();
for (k, v) in tags {
if let Some(s) = v.as_str() {
map.insert(k.clone(), s.to_string());
}
}
map
} else {
BTreeMap::new()
};
let parsed = crate::template::parse_template(&template_body, &child_parameters)
.map_err(|e| format!("Failed to parse nested template: {e}"))?;
let child_stack_name = format!(
"{}-Nested-{}",
resource.logical_id,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
let child_stack_id = format!(
"arn:aws:cloudformation:{}:{}:stack/{}/{}",
self.region,
self.account_id,
child_stack_name,
Uuid::new_v4()
);
let child_provisioner = ResourceProvisioner {
sqs_state: self.sqs_state.clone(),
sns_state: self.sns_state.clone(),
ssm_state: self.ssm_state.clone(),
iam_state: self.iam_state.clone(),
s3_state: self.s3_state.clone(),
eventbridge_state: self.eventbridge_state.clone(),
dynamodb_state: self.dynamodb_state.clone(),
logs_state: self.logs_state.clone(),
lambda_state: self.lambda_state.clone(),
secretsmanager_state: self.secretsmanager_state.clone(),
kinesis_state: self.kinesis_state.clone(),
kms_state: self.kms_state.clone(),
ecr_state: self.ecr_state.clone(),
cloudwatch_state: self.cloudwatch_state.clone(),
elbv2_state: self.elbv2_state.clone(),
organizations_state: self.organizations_state.clone(),
cognito_state: self.cognito_state.clone(),
rds_state: self.rds_state.clone(),
ecs_state: self.ecs_state.clone(),
acm_state: self.acm_state.clone(),
elasticache_state: self.elasticache_state.clone(),
route53_state: self.route53_state.clone(),
cloudfront_state: self.cloudfront_state.clone(),
stepfunctions_state: self.stepfunctions_state.clone(),
wafv2_state: self.wafv2_state.clone(),
apigateway_state: self.apigateway_state.clone(),
apigatewayv2_state: self.apigatewayv2_state.clone(),
ses_state: self.ses_state.clone(),
app_autoscaling_state: self.app_autoscaling_state.clone(),
athena_state: self.athena_state.clone(),
firehose_state: self.firehose_state.clone(),
glue_state: self.glue_state.clone(),
cloudformation_state: self.cloudformation_state.clone(),
delivery: self.delivery.clone(),
account_id: self.account_id.clone(),
region: self.region.clone(),
stack_id: child_stack_id.clone(),
};
let child_resources = crate::service::provision_stack_resources(
&child_provisioner,
&parsed.resources,
&template_body,
&child_parameters,
)
.map_err(|e| format!("Failed to provision nested stack: {e}"))?;
let child_outputs = parsed.outputs;
let stack = crate::state::Stack {
name: child_stack_name.clone(),
stack_id: child_stack_id.clone(),
template: template_body.clone(),
status: "CREATE_COMPLETE".to_string(),
resources: child_resources.clone(),
parameters: child_parameters,
tags: child_tags,
created_at: Utc::now(),
updated_at: None,
description: parsed.description,
notification_arns: Vec::new(),
outputs: child_outputs
.iter()
.map(|o| crate::state::StackOutput {
key: o.logical_id.clone(),
value: o.value.clone(),
description: o.description.clone(),
export_name: o.export_name.clone(),
})
.collect(),
};
{
let mut accounts = self.cloudformation_state.write();
let state = accounts.get_or_create(&self.account_id);
state.stacks.insert(child_stack_name.clone(), stack);
crate::service::record_stack_status_event(
state,
&child_stack_id,
&child_stack_name,
"AWS::CloudFormation::Stack",
"CREATE_IN_PROGRESS",
);
let changes: Vec<crate::service::ResourceChange> = child_resources
.iter()
.map(|r| crate::service::ResourceChange {
action: crate::service::ResourceChangeAction::Create,
logical_id: r.logical_id.clone(),
physical_id: r.physical_id.clone(),
resource_type: r.resource_type.clone(),
})
.collect();
crate::service::record_stack_events(
state,
&child_stack_id,
&child_stack_name,
&changes,
);
crate::service::record_stack_status_event(
state,
&child_stack_id,
&child_stack_name,
"AWS::CloudFormation::Stack",
"CREATE_COMPLETE",
);
}
let mut result = ProvisionResult::new(child_stack_id.clone());
for output in &child_outputs {
result.attributes.insert(
format!("Outputs.{}", output.logical_id),
output.value.clone(),
);
}
Ok(result)
}
fn delete_cloudformation_stack(&self, physical_id: &str) -> Result<(), String> {
let stack = {
let accounts = self.cloudformation_state.read();
let state = accounts.get(&self.account_id);
state.and_then(|s| {
s.stacks
.values()
.find(|st| st.stack_id == physical_id)
.cloned()
})
};
if let Some(stack) = stack {
let stack_name = stack.name.clone();
let stack_id = stack.stack_id.clone();
for resource in stack.resources.iter().rev() {
let _ = self.delete_resource(resource);
}
{
let mut accounts = self.cloudformation_state.write();
let state = accounts.get_or_create(&self.account_id);
state.stacks.remove(&stack_name);
crate::service::record_stack_status_event(
state,
&stack_id,
&stack_name,
"AWS::CloudFormation::Stack",
"DELETE_IN_PROGRESS",
);
crate::service::record_stack_status_event(
state,
&stack_id,
&stack_name,
"AWS::CloudFormation::Stack",
"DELETE_COMPLETE",
);
}
}
Ok(())
}
fn get_att_cloudformation_stack(&self, physical_id: &str, attribute: &str) -> Option<String> {
let accounts = self.cloudformation_state.read();
let state = accounts.get(&self.account_id)?;
let stack = state.stacks.values().find(|s| s.stack_id == physical_id)?;
if let Some(output_key) = attribute.strip_prefix("Outputs.") {
return stack
.outputs
.iter()
.find(|o| o.key == output_key)
.map(|o| o.value.clone());
}
match attribute {
"Outputs" => Some(
serde_json::to_string(
&stack
.outputs
.iter()
.map(|o| (o.key.clone(), o.value.clone()))
.collect::<std::collections::BTreeMap<String, String>>(),
)
.unwrap_or_default(),
),
_ => None,
}
}
fn fetch_template_from_url(&self, url: &str) -> Result<String, String> {
if let Some(rest) = url.strip_prefix("s3://") {
let parts: Vec<&str> = rest.splitn(2, '/').collect();
if parts.len() != 2 {
return Err("Invalid s3:// URL".to_string());
}
return self.fetch_s3_template(parts[0], parts[1]);
}
if let Some(rest) = url.strip_prefix("https://s3.amazonaws.com/") {
let parts: Vec<&str> = rest.splitn(2, '/').collect();
if parts.len() != 2 {
return Err("Invalid S3 HTTPS URL".to_string());
}
return self.fetch_s3_template(parts[0], parts[1]);
}
if let Some(host_rest) = url.strip_prefix("https://") {
if let Some(slash_pos) = host_rest.find('/') {
let host = &host_rest[..slash_pos];
let key = &host_rest[slash_pos + 1..];
if let Some(bucket) = host.strip_suffix(".s3.amazonaws.com") {
return self.fetch_s3_template(bucket, key);
}
if host.contains(".s3.") && host.ends_with(".amazonaws.com") {
let bucket = host.split(".s3.").next().unwrap_or("");
if !bucket.is_empty() {
return self.fetch_s3_template(bucket, key);
}
}
}
}
Err(format!("Unsupported TemplateURL: {url}"))
}
fn fetch_s3_template(&self, bucket: &str, key: &str) -> Result<String, String> {
let mut s3_accounts = self.s3_state.write();
let s3_state = s3_accounts.get_or_create(&self.account_id);
let bucket_obj = s3_state
.buckets
.get(bucket)
.ok_or_else(|| format!("S3 bucket not found: {bucket}"))?;
let obj = bucket_obj
.objects
.get(key)
.ok_or_else(|| format!("S3 object not found: {bucket}/{key}"))?;
let bytes = s3_state
.read_body(&obj.body)
.map_err(|e| format!("Failed to read S3 object body: {e}"))?;
String::from_utf8(bytes.to_vec()).map_err(|e| format!("S3 object is not valid UTF-8: {e}"))
}
}
fn generate_secret_string_payload(gen: &serde_json::Value) -> Result<String, String> {
let length = gen
.get("PasswordLength")
.and_then(|v| v.as_i64())
.unwrap_or(32) as usize;
let exclude_lowercase = gen
.get("ExcludeLowercase")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let exclude_uppercase = gen
.get("ExcludeUppercase")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let exclude_numbers = gen
.get("ExcludeNumbers")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let exclude_punctuation = gen
.get("ExcludePunctuation")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let include_space = gen
.get("IncludeSpace")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let exclude_chars = gen
.get("ExcludeCharacters")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let lowercase = "abcdefghijklmnopqrstuvwxyz";
let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let digits = "0123456789";
let punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
let mut pool = String::new();
if !exclude_lowercase {
pool.extend(lowercase.chars().filter(|c| !exclude_chars.contains(*c)));
}
if !exclude_uppercase {
pool.extend(uppercase.chars().filter(|c| !exclude_chars.contains(*c)));
}
if !exclude_numbers {
pool.extend(digits.chars().filter(|c| !exclude_chars.contains(*c)));
}
if !exclude_punctuation {
pool.extend(punctuation.chars().filter(|c| !exclude_chars.contains(*c)));
}
if include_space && !exclude_chars.contains(' ') {
pool.push(' ');
}
if pool.is_empty() {
return Err("GenerateSecretString character pool is empty".to_string());
}
let pool_chars: Vec<char> = pool.chars().collect();
let mut password = String::with_capacity(length);
let mut counter: u64 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
while password.len() < length {
counter = counter.wrapping_add(0x9E3779B97F4A7C15);
let mut z = counter;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^= z >> 31;
let idx = (z as usize) % pool_chars.len();
password.push(pool_chars[idx]);
}
let template = gen.get("SecretStringTemplate").and_then(|v| v.as_str());
let key = gen.get("GenerateStringKey").and_then(|v| v.as_str());
match (template, key) {
(Some(tmpl), Some(k)) => {
let mut value: serde_json::Value = serde_json::from_str(tmpl)
.map_err(|e| format!("SecretStringTemplate is not valid JSON: {e}"))?;
if let Some(obj) = value.as_object_mut() {
obj.insert(k.to_string(), serde_json::Value::String(password));
Ok(value.to_string())
} else {
Err("SecretStringTemplate must be a JSON object".to_string())
}
}
_ => Ok(password),
}
}
fn parse_ses_receipt_action(value: &serde_json::Value) -> Option<SesReceiptAction> {
let obj = value.as_object()?;
if let Some(s3) = obj.get("S3Action").and_then(|v| v.as_object()) {
let bucket_name = s3.get("BucketName").and_then(|v| v.as_str())?.to_string();
return Some(SesReceiptAction::S3 {
bucket_name,
object_key_prefix: s3
.get("ObjectKeyPrefix")
.and_then(|v| v.as_str())
.map(String::from),
topic_arn: s3
.get("TopicArn")
.and_then(|v| v.as_str())
.map(String::from),
kms_key_arn: s3
.get("KmsKeyArn")
.and_then(|v| v.as_str())
.map(String::from),
});
}
if let Some(sns) = obj.get("SNSAction").and_then(|v| v.as_object()) {
return Some(SesReceiptAction::Sns {
topic_arn: sns.get("TopicArn").and_then(|v| v.as_str())?.to_string(),
encoding: sns
.get("Encoding")
.and_then(|v| v.as_str())
.map(String::from),
});
}
if let Some(la) = obj.get("LambdaAction").and_then(|v| v.as_object()) {
return Some(SesReceiptAction::Lambda {
function_arn: la.get("FunctionArn").and_then(|v| v.as_str())?.to_string(),
invocation_type: la
.get("InvocationType")
.and_then(|v| v.as_str())
.map(String::from),
topic_arn: la
.get("TopicArn")
.and_then(|v| v.as_str())
.map(String::from),
});
}
if let Some(b) = obj.get("BounceAction").and_then(|v| v.as_object()) {
return Some(SesReceiptAction::Bounce {
smtp_reply_code: b
.get("SmtpReplyCode")
.and_then(|v| v.as_str())
.unwrap_or("550")
.to_string(),
message: b
.get("Message")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
sender: b
.get("Sender")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
status_code: b
.get("StatusCode")
.and_then(|v| v.as_str())
.map(String::from),
topic_arn: b.get("TopicArn").and_then(|v| v.as_str()).map(String::from),
});
}
if let Some(ah) = obj.get("AddHeaderAction").and_then(|v| v.as_object()) {
return Some(SesReceiptAction::AddHeader {
header_name: ah.get("HeaderName").and_then(|v| v.as_str())?.to_string(),
header_value: ah.get("HeaderValue").and_then(|v| v.as_str())?.to_string(),
});
}
if let Some(s) = obj.get("StopAction").and_then(|v| v.as_object()) {
return Some(SesReceiptAction::Stop {
scope: s
.get("Scope")
.and_then(|v| v.as_str())
.unwrap_or("RuleSet")
.to_string(),
topic_arn: s.get("TopicArn").and_then(|v| v.as_str()).map(String::from),
});
}
None
}
fn make_apigwv2_id(n: usize) -> String {
let s = uuid::Uuid::new_v4().simple().to_string();
s[..n.min(s.len())].to_string()
}
fn cfn_as_i64(v: &serde_json::Value) -> Option<i64> {
if let Some(n) = v.as_i64() {
return Some(n);
}
v.as_str().and_then(|s| s.parse::<i64>().ok())
}
fn lowercase_first_keys(value: serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::Object(map) => {
let mut out = serde_json::Map::new();
for (k, v) in map {
let new_key = if let Some(first) = k.chars().next() {
let mut s = String::with_capacity(k.len());
s.extend(first.to_lowercase());
s.push_str(&k[first.len_utf8()..]);
s
} else {
k
};
out.insert(new_key, lowercase_first_keys(v));
}
serde_json::Value::Object(out)
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.into_iter().map(lowercase_first_keys).collect())
}
other => other,
}
}
fn synth_acm_domain_validation(
domain_name: &str,
sans: &[String],
validation_method: &str,
) -> Vec<AcmDomainValidation> {
let mut all = vec![domain_name.to_string()];
for s in sans {
if !all.contains(s) {
all.push(s.clone());
}
}
all.into_iter()
.map(|name| AcmDomainValidation {
domain_name: name.clone(),
validation_status: "SUCCESS".to_string(),
validation_method: validation_method.to_string(),
resource_record_name: Some(format!("_amzn-validations.{name}.")),
resource_record_type: Some("CNAME".to_string()),
resource_record_value: Some(format!("{}.acm-validations.aws.", Uuid::new_v4())),
})
.collect()
}
fn parse_acm_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
if let Some(arr) = value.and_then(|v| v.as_array()) {
for t in arr {
if let (Some(k), Some(v)) = (
t.get("Key").and_then(|v| v.as_str()),
t.get("Value").and_then(|v| v.as_str()),
) {
out.insert(k.to_string(), v.to_string());
}
}
}
out
}
fn parse_ecs_tags(value: Option<&serde_json::Value>) -> Vec<EcsTagEntry> {
let Some(arr) = value.and_then(|v| v.as_array()) else {
return Vec::new();
};
arr.iter()
.filter_map(|t| {
let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
Some(EcsTagEntry { key, value })
})
.collect()
}
fn parse_ecs_cluster_name(input: &str) -> String {
if let Some(after) = input.split(":cluster/").nth(1) {
return after.to_string();
}
input.to_string()
}
fn parse_td_arn(input: &str) -> (String, i32) {
let suffix = input.rsplit('/').next().unwrap_or(input);
if let Some((family, rev)) = suffix.split_once(':') {
if let Ok(revision) = rev.parse::<i32>() {
return (family.to_string(), revision);
}
}
(input.to_string(), 1)
}
fn parse_service_arn(input: &str) -> Option<(String, String)> {
let after = input.split(":service/").nth(1)?;
let mut parts = after.splitn(2, '/');
let cluster = parts.next()?.to_string();
let service = parts.next()?.to_string();
Some((cluster, service))
}
fn parse_rds_tags(value: Option<&serde_json::Value>) -> Vec<RdsTag> {
let Some(arr) = value.and_then(|v| v.as_array()) else {
return Vec::new();
};
arr.iter()
.filter_map(|t| {
let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
Some(RdsTag { key, value })
})
.collect()
}
fn rds_extras_mut<'a>(
state: &'a mut fakecloud_rds::RdsState,
category: &str,
) -> &'a mut BTreeMap<String, serde_json::Value> {
state.extras.entry(category.to_string()).or_default()
}
fn parse_cognito_string_array(value: Option<&serde_json::Value>) -> Vec<String> {
value
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default()
}
fn parse_cognito_password_policy(value: Option<&serde_json::Value>) -> PasswordPolicy {
let Some(inner) = value
.and_then(|v| v.get("PasswordPolicy"))
.and_then(|v| v.as_object())
else {
return PasswordPolicy::default();
};
let mut p = PasswordPolicy::default();
if let Some(n) = inner.get("MinimumLength").and_then(|v| v.as_i64()) {
p.minimum_length = n;
}
if let Some(b) = inner.get("RequireUppercase").and_then(|v| v.as_bool()) {
p.require_uppercase = b;
}
if let Some(b) = inner.get("RequireLowercase").and_then(|v| v.as_bool()) {
p.require_lowercase = b;
}
if let Some(b) = inner.get("RequireNumbers").and_then(|v| v.as_bool()) {
p.require_numbers = b;
}
if let Some(b) = inner.get("RequireSymbols").and_then(|v| v.as_bool()) {
p.require_symbols = b;
}
if let Some(n) = inner
.get("TemporaryPasswordValidityDays")
.and_then(|v| v.as_i64())
{
p.temporary_password_validity_days = n;
}
p
}
fn parse_cognito_schema_attribute(value: &serde_json::Value) -> Option<SchemaAttribute> {
let name = value.get("Name").and_then(|v| v.as_str())?.to_string();
Some(SchemaAttribute {
name,
attribute_data_type: value
.get("AttributeDataType")
.and_then(|v| v.as_str())
.unwrap_or("String")
.to_string(),
developer_only_attribute: value
.get("DeveloperOnlyAttribute")
.and_then(|v| v.as_bool())
.unwrap_or(false),
mutable: value
.get("Mutable")
.and_then(|v| v.as_bool())
.unwrap_or(true),
required: value
.get("Required")
.and_then(|v| v.as_bool())
.unwrap_or(false),
string_attribute_constraints: None,
number_attribute_constraints: None,
})
}
fn parse_cognito_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
if let Some(obj) = value.and_then(|v| v.as_object()) {
for (k, v) in obj {
if let Some(s) = v.as_str() {
out.insert(k.clone(), s.to_string());
}
}
}
out
}
fn parse_cognito_email_configuration(
value: Option<&serde_json::Value>,
) -> Option<EmailConfiguration> {
let inner = value?.as_object()?;
Some(EmailConfiguration {
source_arn: inner
.get("SourceArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
reply_to_email_address: inner
.get("ReplyToEmailAddress")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
email_sending_account: inner
.get("EmailSendingAccount")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
from_email_address: inner
.get("From")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
configuration_set: inner
.get("ConfigurationSet")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
})
}
fn parse_cognito_sms_configuration(value: Option<&serde_json::Value>) -> Option<SmsConfiguration> {
let inner = value?.as_object()?;
Some(SmsConfiguration {
sns_caller_arn: inner
.get("SnsCallerArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
external_id: inner
.get("ExternalId")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
sns_region: inner
.get("SnsRegion")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
})
}
fn parse_cognito_admin_create_user_config(
value: Option<&serde_json::Value>,
) -> Option<AdminCreateUserConfig> {
let inner = value?.as_object()?;
Some(AdminCreateUserConfig {
allow_admin_create_user_only: inner
.get("AllowAdminCreateUserOnly")
.and_then(|v| v.as_bool()),
invite_message_template: None,
unused_account_validity_days: inner
.get("UnusedAccountValidityDays")
.and_then(|v| v.as_i64()),
})
}
fn parse_cognito_account_recovery(
value: Option<&serde_json::Value>,
) -> Option<AccountRecoverySetting> {
let arr = value?.get("RecoveryMechanisms")?.as_array()?;
Some(AccountRecoverySetting {
recovery_mechanisms: arr
.iter()
.filter_map(|m| {
let name = m.get("Name").and_then(|v| v.as_str())?.to_string();
let priority = m.get("Priority").and_then(|v| v.as_i64()).unwrap_or(1);
Some(RecoveryOption { name, priority })
})
.collect(),
})
}
fn parse_firehose_s3_destination(value: &serde_json::Value) -> Result<S3Destination, String> {
let role_arn = value
.get("RoleARN")
.and_then(|v| v.as_str())
.ok_or("S3 destination requires RoleARN")?
.to_string();
let bucket_arn = value
.get("BucketARN")
.and_then(|v| v.as_str())
.ok_or("S3 destination requires BucketARN")?
.to_string();
let prefix = value
.get("Prefix")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let error_output_prefix = value
.get("ErrorOutputPrefix")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut buffering_size_mb = None;
let mut buffering_interval_seconds = None;
if let Some(hints) = value.get("BufferingHints") {
buffering_size_mb = hints.get("SizeInMBs").and_then(|v| v.as_i64());
buffering_interval_seconds = hints.get("IntervalInSeconds").and_then(|v| v.as_i64());
}
let compression_format = value
.get("CompressionFormat")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
Ok(S3Destination {
destination_id: "destination-1".to_string(),
role_arn,
bucket_arn,
prefix,
error_output_prefix,
buffering_size_mb,
buffering_interval_seconds,
compression_format,
})
}
#[cfg(test)]
mod tests {
use super::*;
use parking_lot::RwLock;
fn make_provisioner() -> ResourceProvisioner {
ResourceProvisioner {
sqs_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new(
"123456789012",
"us-east-1",
"http://localhost:4566",
),
)),
sns_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new(
"123456789012",
"us-east-1",
"http://localhost:4566",
),
)),
ssm_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new(
"123456789012",
"us-east-1",
"http://localhost:4566",
),
)),
iam_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "http://localhost:4566"),
)),
s3_state: Arc::new(RwLock::new(fakecloud_core::multi_account::MultiAccountState::new(
"123456789012",
"us-east-1", "",
))),
eventbridge_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
dynamodb_state: Arc::new(RwLock::new(fakecloud_core::multi_account::MultiAccountState::new(
"123456789012",
"us-east-1", "",
))),
logs_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
lambda_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
secretsmanager_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
kinesis_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
kms_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
ecr_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
cloudwatch_state: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
elbv2_state: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
organizations_state: Arc::new(RwLock::new(None)),
cognito_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
rds_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
ecs_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
acm_state: Arc::new(RwLock::new(fakecloud_acm::AcmAccounts::new())),
elasticache_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
route53_state: Arc::new(RwLock::new(fakecloud_route53::Route53Accounts::new())),
cloudfront_state: Arc::new(RwLock::new(
fakecloud_cloudfront::CloudFrontAccounts::new(),
)),
cloudformation_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
)),
stepfunctions_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new(
"123456789012",
"us-east-1",
"",
),
)),
wafv2_state: Arc::new(RwLock::new(fakecloud_wafv2::Wafv2Accounts::default())),
apigateway_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new(
"123456789012",
"us-east-1",
"",
),
)),
apigatewayv2_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new(
"123456789012",
"us-east-1",
"",
),
)),
ses_state: Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new(
"123456789012",
"us-east-1",
"",
),
)),
app_autoscaling_state: Arc::new(parking_lot::RwLock::new(
fakecloud_application_autoscaling::ApplicationAutoScalingAccounts::new(),
)),
athena_state: Arc::new(parking_lot::RwLock::new(
fakecloud_athena::AthenaAccounts::new(),
)),
firehose_state: Arc::new(parking_lot::RwLock::new(
fakecloud_firehose::FirehoseAccounts::new(),
)),
glue_state: Arc::new(parking_lot::RwLock::new(
fakecloud_glue::GlueAccounts::new(),
)),
delivery: Arc::new(DeliveryBus::new()),
account_id: "123456789012".to_string(),
region: "us-east-1".to_string(),
stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test/00000000-0000-0000-0000-000000000000".to_string(),
}
}
fn make_resource(
resource_type: &str,
logical_id: &str,
props: serde_json::Value,
) -> ResourceDefinition {
ResourceDefinition {
logical_id: logical_id.to_string(),
resource_type: resource_type.to_string(),
properties: props,
}
}
#[test]
fn sns_subscription_rejects_nonexistent_topic() {
let prov = make_provisioner();
let resource = make_resource(
"AWS::SNS::Subscription",
"MySub",
serde_json::json!({
"TopicArn": "arn:aws:sns:us-east-1:123456789012:NonExistent",
"Protocol": "sqs",
"Endpoint": "arn:aws:sqs:us-east-1:123456789012:my-queue"
}),
);
let result = prov.create_resource(&resource);
assert!(result.is_err());
assert!(result.unwrap_err().contains("does not exist"));
}
#[test]
fn sns_subscription_succeeds_when_topic_exists() {
let prov = make_provisioner();
let topic = make_resource(
"AWS::SNS::Topic",
"MyTopic",
serde_json::json!({ "TopicName": "my-topic" }),
);
let topic_result = prov.create_resource(&topic);
assert!(topic_result.is_ok());
let topic_arn = topic_result.unwrap().physical_id;
let sub = make_resource(
"AWS::SNS::Subscription",
"MySub",
serde_json::json!({
"TopicArn": topic_arn,
"Protocol": "sqs",
"Endpoint": "arn:aws:sqs:us-east-1:123456789012:my-queue"
}),
);
let result = prov.create_resource(&sub);
assert!(result.is_ok());
}
#[test]
fn eventbridge_rule_arn_default_bus_omits_bus_name() {
let prov = make_provisioner();
let resource = make_resource(
"AWS::Events::Rule",
"MyRule",
serde_json::json!({
"Name": "my-rule",
"ScheduleExpression": "rate(1 hour)"
}),
);
let result = prov.create_resource(&resource).unwrap();
assert_eq!(
result.physical_id,
"arn:aws:events:us-east-1:123456789012:rule/my-rule"
);
assert!(!result.physical_id.contains("rule/default/"));
}
#[test]
fn eventbridge_rule_arn_custom_bus_includes_bus_name() {
let prov = make_provisioner();
{
let mut eb_accounts = prov.eventbridge_state.write();
let state = eb_accounts.default_mut();
state.buses.insert(
"custom-bus".to_string(),
fakecloud_eventbridge::EventBus {
name: "custom-bus".to_string(),
arn: "arn:aws:events:us-east-1:123456789012:event-bus/custom-bus".to_string(),
policy: None,
creation_time: Utc::now(),
last_modified_time: Utc::now(),
description: None,
kms_key_identifier: None,
dead_letter_config: None,
tags: std::collections::BTreeMap::new(),
},
);
}
let resource = make_resource(
"AWS::Events::Rule",
"MyRule",
serde_json::json!({
"Name": "my-rule",
"EventBusName": "custom-bus",
"ScheduleExpression": "rate(1 hour)"
}),
);
let result = prov.create_resource(&resource).unwrap();
assert_eq!(
result.physical_id,
"arn:aws:events:us-east-1:123456789012:rule/custom-bus/my-rule"
);
}
#[test]
fn eventbridge_rule_rejects_nonexistent_bus() {
let prov = make_provisioner();
let resource = make_resource(
"AWS::Events::Rule",
"MyRule",
serde_json::json!({
"Name": "my-rule",
"EventBusName": "nonexistent-bus",
"ScheduleExpression": "rate(1 hour)"
}),
);
let result = prov.create_resource(&resource);
assert!(result.is_err());
assert!(result.unwrap_err().contains("does not exist"));
}
#[test]
fn custom_resource_requires_service_token() {
let prov = make_provisioner();
let resource = make_resource(
"Custom::MyResource",
"MyCustom",
serde_json::json!({
"Foo": "bar"
}),
);
let result = prov.create_resource(&resource);
assert!(result.is_err());
assert!(
result.unwrap_err().contains("ServiceToken"),
"Should require ServiceToken property"
);
}
#[test]
fn custom_resource_succeeds_without_lambda_delivery() {
let prov = make_provisioner();
let resource = make_resource(
"Custom::MyResource",
"MyCustom",
serde_json::json!({
"ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:my-func",
"Foo": "bar"
}),
);
let result = prov.create_resource(&resource);
assert!(result.is_ok());
let sr = result.unwrap();
assert_eq!(sr.logical_id, "MyCustom");
assert_eq!(sr.resource_type, "Custom::MyResource");
assert!(sr.physical_id.starts_with("MyCustom-"));
}
#[test]
fn cloudformation_custom_resource_type_succeeds() {
let prov = make_provisioner();
let resource = make_resource(
"AWS::CloudFormation::CustomResource",
"MyCustom2",
serde_json::json!({
"ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:my-func",
"Key": "value"
}),
);
let result = prov.create_resource(&resource);
assert!(result.is_ok());
let sr = result.unwrap();
assert_eq!(sr.resource_type, "AWS::CloudFormation::CustomResource");
}
#[test]
fn sqs_queue_create_and_delete() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SQS::Queue",
"MyQ",
serde_json::json!({"QueueName": "my-q"}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.contains("my-q"));
assert_eq!(sr.resource_type, "AWS::SQS::Queue");
prov.delete_resource(&sr).unwrap();
}
#[test]
fn sqs_queue_fifo_with_suffix() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SQS::Queue",
"FifoQ",
serde_json::json!({"QueueName": "my-fifo.fifo", "FifoQueue": true}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.contains(".fifo"));
}
#[test]
fn sns_topic_create_and_delete() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SNS::Topic",
"MyTopic",
serde_json::json!({"TopicName": "t1"}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.contains("t1"));
prov.delete_resource(&sr).unwrap();
}
#[test]
fn ssm_parameter_create_and_delete() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SSM::Parameter",
"MyParam",
serde_json::json!({
"Name": "/my/param",
"Type": "String",
"Value": "v1"
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "/my/param");
prov.delete_resource(&sr).unwrap();
}
#[test]
fn iam_role_create_and_delete() {
let prov = make_provisioner();
let res = make_resource(
"AWS::IAM::Role",
"MyRole",
serde_json::json!({
"RoleName": "my-role",
"AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.contains("my-role"));
prov.delete_resource(&sr).unwrap();
}
#[test]
fn iam_policy_create_and_delete() {
let prov = make_provisioner();
let res = make_resource(
"AWS::IAM::Policy",
"MyPolicy",
serde_json::json!({
"PolicyName": "my-policy",
"PolicyDocument": {"Version": "2012-10-17", "Statement": []}
}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.contains("my-policy"));
prov.delete_resource(&sr).unwrap();
}
#[test]
fn s3_bucket_create_and_delete() {
let prov = make_provisioner();
let res = make_resource(
"AWS::S3::Bucket",
"MyBucket",
serde_json::json!({"BucketName": "my-bucket"}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "my-bucket");
prov.delete_resource(&sr).unwrap();
}
#[test]
fn dynamodb_table_create_and_delete() {
let prov = make_provisioner();
let res = make_resource(
"AWS::DynamoDB::Table",
"MyTable",
serde_json::json!({
"TableName": "my-table",
"KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
"AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
"BillingMode": "PAY_PER_REQUEST"
}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.contains("my-table"));
prov.delete_resource(&sr).unwrap();
}
#[test]
fn log_group_create_and_delete() {
let prov = make_provisioner();
let res = make_resource(
"AWS::Logs::LogGroup",
"MyLogs",
serde_json::json!({"LogGroupName": "/app/logs"}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.contains("/app/logs"));
prov.delete_resource(&sr).unwrap();
}
#[test]
fn lambda_function_create_and_delete() {
let prov = make_provisioner();
let res = make_resource(
"AWS::Lambda::Function",
"MyFn",
serde_json::json!({
"FunctionName": "my-fn",
"Runtime": "nodejs20.x",
"Role": "arn:aws:iam::123456789012:role/lambda-role",
"Handler": "index.handler",
"MemorySize": 256,
"Timeout": 10,
"Environment": {"Variables": {"FOO": "bar"}}
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "my-fn");
assert_eq!(
sr.attributes.get("Arn").map(String::as_str),
Some("arn:aws:lambda:us-east-1:123456789012:function:my-fn")
);
{
let lam = prov.lambda_state.read();
let st = lam.get("123456789012").unwrap();
let f = st.functions.get("my-fn").unwrap();
assert_eq!(f.runtime, "nodejs20.x");
assert_eq!(f.memory_size, 256);
assert_eq!(f.environment.get("FOO").unwrap(), "bar");
}
prov.delete_resource(&sr).unwrap();
let lam = prov.lambda_state.read();
let st = lam.get("123456789012").unwrap();
assert!(!st.functions.contains_key("my-fn"));
}
#[test]
fn unsupported_resource_type_fails() {
let prov = make_provisioner();
let res = make_resource("AWS::NonExistent::Thing", "X", serde_json::json!({}));
assert!(prov.create_resource(&res).is_err());
}
#[test]
fn iam_role_with_inline_policies() {
let prov = make_provisioner();
let res = make_resource(
"AWS::IAM::Role",
"MyRole",
serde_json::json!({
"RoleName": "role-inline",
"AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []},
"Policies": [
{
"PolicyName": "inline-1",
"PolicyDocument": {"Version": "2012-10-17", "Statement": []}
}
]
}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.contains("role-inline"));
}
#[test]
fn sqs_queue_auto_name() {
let prov = make_provisioner();
let res = make_resource("AWS::SQS::Queue", "AutoQ", serde_json::json!({}));
let sr = prov.create_resource(&res).unwrap();
assert!(!sr.physical_id.is_empty());
}
#[test]
fn sns_topic_auto_name() {
let prov = make_provisioner();
let res = make_resource("AWS::SNS::Topic", "AutoT", serde_json::json!({}));
let sr = prov.create_resource(&res).unwrap();
assert!(!sr.physical_id.is_empty());
}
#[test]
fn unsupported_resource_type_errors() {
let prov = make_provisioner();
let res = make_resource("AWS::FooBar::Thing", "X", serde_json::json!({}));
assert!(prov.create_resource(&res).is_err());
}
#[test]
fn sqs_queue_with_redrive_policy() {
let prov = make_provisioner();
let dlq = make_resource(
"AWS::SQS::Queue",
"DLQ",
serde_json::json!({"QueueName": "dlq1"}),
);
let dlq_resource = prov.create_resource(&dlq).unwrap();
let _ = dlq_resource.physical_id;
let src = make_resource(
"AWS::SQS::Queue",
"Src",
serde_json::json!({
"QueueName": "src1",
"RedrivePolicy": {
"deadLetterTargetArn": "arn:aws:sqs:us-east-1:123456789012:dlq1",
"maxReceiveCount": 3
}
}),
);
let sr = prov.create_resource(&src).unwrap();
assert!(!sr.physical_id.is_empty());
}
#[test]
fn sns_topic_with_display_name() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SNS::Topic",
"WithName",
serde_json::json!({"TopicName": "named-topic", "DisplayName": "Named"}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.contains("named-topic"));
}
#[test]
fn ssm_parameter_with_explicit_name() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SSM::Parameter",
"Param",
serde_json::json!({"Name": "/my/param", "Value": "v", "Type": "String"}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.contains("/my/param"));
}
#[test]
fn ssm_parameter_missing_name_errors() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SSM::Parameter",
"AutoP",
serde_json::json!({"Value": "v", "Type": "String"}),
);
assert!(prov.create_resource(&res).is_err());
}
#[test]
fn iam_managed_policy_auto_name() {
let prov = make_provisioner();
let res = make_resource(
"AWS::IAM::Policy",
"AutoPol",
serde_json::json!({
"PolicyName": "inline-pol",
"PolicyDocument": {"Version": "2012-10-17", "Statement": []},
"Users": []
}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(!sr.physical_id.is_empty());
}
#[test]
fn delete_resource_works_for_queue() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SQS::Queue",
"ToDel",
serde_json::json!({"QueueName": "todel"}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(prov.delete_resource(&sr).is_ok());
}
#[test]
fn delete_resource_works_for_topic() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SNS::Topic",
"DelT",
serde_json::json!({"TopicName": "delt"}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(prov.delete_resource(&sr).is_ok());
}
#[test]
fn application_autoscaling_scalable_target_round_trip() {
let prov = make_provisioner();
let res = make_resource(
"AWS::ApplicationAutoScaling::ScalableTarget",
"Target",
serde_json::json!({
"ServiceNamespace": "ecs",
"ResourceId": "service/my-cluster/my-service",
"ScalableDimension": "ecs:service:DesiredCount",
"MinCapacity": 1,
"MaxCapacity": 10,
"RoleARN": "arn:aws:iam::123456789012:role/my-role",
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "service/my-cluster/my-service");
assert!(sr.attributes.contains_key("ScalableTargetARN"));
assert!(prov.delete_resource(&sr).is_ok());
}
#[test]
fn application_autoscaling_scaling_policy_requires_target() {
let prov = make_provisioner();
let res = make_resource(
"AWS::ApplicationAutoScaling::ScalingPolicy",
"Policy",
serde_json::json!({
"PolicyName": "my-policy",
"ServiceNamespace": "ecs",
"ResourceId": "service/my-cluster/my-service",
"ScalableDimension": "ecs:service:DesiredCount",
"PolicyType": "TargetTrackingScaling",
"TargetTrackingScalingPolicyConfiguration": {
"TargetValue": 50.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
}
},
}),
);
assert!(prov.create_resource(&res).is_err());
}
#[test]
fn application_autoscaling_scaling_policy_round_trip() {
let prov = make_provisioner();
let target = make_resource(
"AWS::ApplicationAutoScaling::ScalableTarget",
"Target",
serde_json::json!({
"ServiceNamespace": "ecs",
"ResourceId": "service/my-cluster/my-service",
"ScalableDimension": "ecs:service:DesiredCount",
"MinCapacity": 1,
"MaxCapacity": 10,
}),
);
let sr = prov.create_resource(&target).unwrap();
let policy = make_resource(
"AWS::ApplicationAutoScaling::ScalingPolicy",
"Policy",
serde_json::json!({
"PolicyName": "my-policy",
"ServiceNamespace": "ecs",
"ResourceId": "service/my-cluster/my-service",
"ScalableDimension": "ecs:service:DesiredCount",
"PolicyType": "TargetTrackingScaling",
"TargetTrackingScalingPolicyConfiguration": {
"TargetValue": 50.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
}
},
}),
);
let psr = prov.create_resource(&policy).unwrap();
assert!(psr.physical_id.starts_with("arn:aws:autoscaling:"));
assert!(prov.delete_resource(&psr).is_ok());
assert!(prov.delete_resource(&sr).is_ok());
}
#[test]
fn sqs_queue_with_fifo_suffix() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SQS::Queue",
"Fifo",
serde_json::json!({"QueueName": "fq.fifo", "FifoQueue": true}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.ends_with(".fifo"));
}
#[test]
fn getatt_s3_bucket_arn_returns_arn() {
let prov = make_provisioner();
let bucket = make_resource(
"AWS::S3::Bucket",
"MyBucket",
serde_json::json!({"BucketName": "my-bucket"}),
);
let sr = prov.create_resource(&bucket).unwrap();
assert_eq!(
prov.get_att(&sr, "Arn"),
Some("arn:aws:s3:::my-bucket".to_string())
);
}
#[test]
fn getatt_s3_bucket_domain_name_returns_dns_name() {
let prov = make_provisioner();
let bucket = make_resource(
"AWS::S3::Bucket",
"MyBucket",
serde_json::json!({"BucketName": "my-bucket"}),
);
let sr = prov.create_resource(&bucket).unwrap();
assert_eq!(
prov.get_att(&sr, "DomainName"),
Some("my-bucket.s3.amazonaws.com".to_string())
);
}
#[test]
fn getatt_lambda_function_arn_returns_function_arn() {
let prov = make_provisioner();
let role = make_resource(
"AWS::IAM::Role",
"MyRole",
serde_json::json!({
"RoleName": "my-role",
"AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
}),
);
let role_sr = prov.create_resource(&role).unwrap();
let fn_res = make_resource(
"AWS::Lambda::Function",
"MyFn",
serde_json::json!({
"FunctionName": "my-fn",
"Runtime": "python3.11",
"Handler": "index.handler",
"Role": role_sr.physical_id,
"Code": {"ZipFile": "def handler(e,c): return e"}
}),
);
let fn_sr = prov.create_resource(&fn_res).unwrap();
let arn = prov.get_att(&fn_sr, "Arn").expect("Arn should resolve");
assert!(arn.starts_with("arn:aws:lambda:"));
assert!(arn.contains(":function:my-fn"));
}
#[test]
fn getatt_iam_role_arn_returns_role_arn() {
let prov = make_provisioner();
let role = make_resource(
"AWS::IAM::Role",
"MyRole",
serde_json::json!({
"RoleName": "my-role",
"AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
}),
);
let sr = prov.create_resource(&role).unwrap();
assert_eq!(
prov.get_att(&sr, "Arn"),
Some("arn:aws:iam::123456789012:role/my-role".to_string())
);
let role_id = prov.get_att(&sr, "RoleId").expect("RoleId should resolve");
assert!(role_id.starts_with("FKIA"));
}
#[test]
fn getatt_unknown_attribute_returns_none() {
let prov = make_provisioner();
let bucket = make_resource(
"AWS::S3::Bucket",
"MyBucket",
serde_json::json!({"BucketName": "my-bucket"}),
);
let sr = prov.create_resource(&bucket).unwrap();
assert_eq!(prov.get_att(&sr, "NotARealAttr"), None);
}
#[test]
fn getatt_unknown_resource_type_returns_none() {
let prov = make_provisioner();
let stack_resource = StackResource {
logical_id: "Mystery".to_string(),
physical_id: "mystery-id".to_string(),
resource_type: "AWS::Made::Up".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: BTreeMap::new(),
};
assert_eq!(prov.get_att(&stack_resource, "Arn"), None);
}
#[test]
fn getatt_falls_back_to_captured_attributes() {
let prov = make_provisioner();
let stack_resource = StackResource {
logical_id: "MyTopic".to_string(),
physical_id: "arn:aws:sns:us-east-1:123456789012:my-topic".to_string(),
resource_type: "AWS::SNS::Topic".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: {
let mut m = BTreeMap::new();
m.insert("TopicArn".to_string(), "captured-arn".to_string());
m
},
};
assert_eq!(
prov.get_att(&stack_resource, "TopicArn"),
Some("captured-arn".to_string())
);
}
#[test]
fn getatt_secrets_manager_arn_resolves_via_live_state() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SecretsManager::Secret",
"MySecret",
serde_json::json!({"Name": "my-secret", "SecretString": "hunter2"}),
);
let sr = prov.create_resource(&res).unwrap();
let arn = prov.get_att(&sr, "Arn").expect("Arn should resolve");
assert!(arn.starts_with("arn:aws:secretsmanager:"));
assert!(arn.ends_with(":secret:my-secret"));
}
#[test]
fn wafv2_web_acl_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::WAFv2::WebACL",
"MyAcl",
serde_json::json!({
"Name": "my-acl",
"Scope": "REGIONAL",
"DefaultAction": {"Allow": {}},
"Rules": [{"Name": "rule1", "Priority": 1, "Statement": {}, "VisibilityConfig": {}}],
"VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "my-acl-metric"},
"Capacity": 100,
}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
assert_eq!(prov.get_att(&sr, "Name"), Some("my-acl".to_string()));
assert!(prov.get_att(&sr, "Id").is_some());
assert_eq!(prov.get_att(&sr, "Capacity"), Some("100".to_string()));
prov.delete_resource(&sr.clone()).unwrap();
let fresh = StackResource {
logical_id: "MyAcl".to_string(),
physical_id: sr.physical_id.clone(),
resource_type: "AWS::WAFv2::WebACL".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: BTreeMap::new(),
};
assert_eq!(prov.get_att(&fresh, "Arn"), None);
}
#[test]
fn wafv2_ip_set_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::WAFv2::IPSet",
"MyIpSet",
serde_json::json!({
"Name": "my-ipset",
"Scope": "REGIONAL",
"IPAddressVersion": "IPV4",
"Addresses": ["10.0.0.0/8"],
}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
assert_eq!(prov.get_att(&sr, "Name"), Some("my-ipset".to_string()));
prov.delete_resource(&sr.clone()).unwrap();
let fresh = StackResource {
logical_id: "MyIpSet".to_string(),
physical_id: sr.physical_id.clone(),
resource_type: "AWS::WAFv2::IPSet".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: BTreeMap::new(),
};
assert_eq!(prov.get_att(&fresh, "Arn"), None);
}
#[test]
fn wafv2_regex_pattern_set_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::WAFv2::RegexPatternSet",
"MyRegexSet",
serde_json::json!({
"Name": "my-regex",
"Scope": "REGIONAL",
"RegularExpressions": [{"RegexString": "^test"}],
}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
assert_eq!(prov.get_att(&sr, "Name"), Some("my-regex".to_string()));
prov.delete_resource(&sr.clone()).unwrap();
let fresh = StackResource {
logical_id: "MyRegexSet".to_string(),
physical_id: sr.physical_id.clone(),
resource_type: "AWS::WAFv2::RegexPatternSet".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: BTreeMap::new(),
};
assert_eq!(prov.get_att(&fresh, "Arn"), None);
}
#[test]
fn wafv2_rule_group_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::WAFv2::RuleGroup",
"MyRuleGroup",
serde_json::json!({
"Name": "my-rg",
"Scope": "REGIONAL",
"Capacity": 50,
"Rules": [{"Name": "r1", "Priority": 1, "Statement": {}, "VisibilityConfig": {}}],
"VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "rg-metric"},
}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
assert_eq!(prov.get_att(&sr, "Name"), Some("my-rg".to_string()));
prov.delete_resource(&sr.clone()).unwrap();
let fresh = StackResource {
logical_id: "MyRuleGroup".to_string(),
physical_id: sr.physical_id.clone(),
resource_type: "AWS::WAFv2::RuleGroup".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: BTreeMap::new(),
};
assert_eq!(prov.get_att(&fresh, "Arn"), None);
}
#[test]
fn wafv2_logging_configuration_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::WAFv2::LoggingConfiguration",
"MyLogConfig",
serde_json::json!({
"ResourceArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/abc",
"LogDestinationConfigs": ["arn:aws:logs:us-east-1:123456789012:log-group:/aws/waf"],
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(
sr.physical_id,
"arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/abc"
);
prov.delete_resource(&sr.clone()).unwrap();
}
#[test]
fn wafv2_web_acl_association_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::WAFv2::WebACLAssociation",
"MyAssoc",
serde_json::json!({
"ResourceArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188",
"WebACLArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/my-acl/abc",
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188");
prov.delete_resource(&sr.clone()).unwrap();
}
#[test]
fn ses_configuration_set_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SES::ConfigurationSet",
"MyConfigSet",
serde_json::json!({
"Name": "my-cs",
"SendingOptions": {"SendingEnabled": true},
"DeliveryOptions": {"TlsPolicy": "REQUIRE"},
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "my-cs");
assert_eq!(prov.get_att(&sr, "Name"), Some("my-cs".to_string()));
prov.delete_resource(&sr.clone()).unwrap();
let fresh = StackResource {
logical_id: "MyConfigSet".to_string(),
physical_id: "my-cs".to_string(),
resource_type: "AWS::SES::ConfigurationSet".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: BTreeMap::new(),
};
assert_eq!(prov.get_att(&fresh, "Name"), None);
}
#[test]
fn ses_email_identity_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SES::EmailIdentity",
"MyIdentity",
serde_json::json!({"EmailIdentity": "example.com"}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "example.com");
assert_eq!(
prov.get_att(&sr, "IdentityName"),
Some("example.com".to_string())
);
prov.delete_resource(&sr.clone()).unwrap();
let fresh = StackResource {
logical_id: "MyIdentity".to_string(),
physical_id: "example.com".to_string(),
resource_type: "AWS::SES::EmailIdentity".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: BTreeMap::new(),
};
assert_eq!(prov.get_att(&fresh, "IdentityName"), None);
}
#[test]
fn ses_template_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SES::Template",
"MyTemplate",
serde_json::json!({
"Template": {
"TemplateName": "my-tpl",
"SubjectPart": "Hello",
"HtmlPart": "<h1>Hi</h1>",
"TextPart": "Hi",
},
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "my-tpl");
assert_eq!(
prov.get_att(&sr, "TemplateName"),
Some("my-tpl".to_string())
);
prov.delete_resource(&sr.clone()).unwrap();
let fresh = StackResource {
logical_id: "MyTemplate".to_string(),
physical_id: "my-tpl".to_string(),
resource_type: "AWS::SES::Template".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: BTreeMap::new(),
};
assert_eq!(prov.get_att(&fresh, "TemplateName"), None);
}
#[test]
fn ses_contact_list_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SES::ContactList",
"MyContactList",
serde_json::json!({
"ContactListName": "my-cl",
"Description": "Test contacts",
"Topics": [{"TopicName": "news", "DisplayName": "Newsletter", "Description": "Weekly news"}],
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "my-cl");
assert_eq!(
prov.get_att(&sr, "ContactListName"),
Some("my-cl".to_string())
);
prov.delete_resource(&sr.clone()).unwrap();
let fresh = StackResource {
logical_id: "MyContactList".to_string(),
physical_id: "my-cl".to_string(),
resource_type: "AWS::SES::ContactList".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: BTreeMap::new(),
};
assert_eq!(prov.get_att(&fresh, "ContactListName"), None);
}
#[test]
fn ses_dedicated_ip_pool_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SES::DedicatedIpPool",
"MyPool",
serde_json::json!({"PoolName": "my-pool", "ScalingMode": "STANDARD"}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "my-pool");
assert_eq!(prov.get_att(&sr, "PoolName"), Some("my-pool".to_string()));
prov.delete_resource(&sr.clone()).unwrap();
let fresh = StackResource {
logical_id: "MyPool".to_string(),
physical_id: "my-pool".to_string(),
resource_type: "AWS::SES::DedicatedIpPool".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: BTreeMap::new(),
};
assert_eq!(prov.get_att(&fresh, "PoolName"), None);
}
#[test]
fn ses_receipt_rule_set_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SES::ReceiptRuleSet",
"MyRuleSet",
serde_json::json!({"RuleSetName": "my-rs"}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "my-rs");
assert_eq!(prov.get_att(&sr, "RuleSetName"), Some("my-rs".to_string()));
prov.delete_resource(&sr.clone()).unwrap();
let fresh = StackResource {
logical_id: "MyRuleSet".to_string(),
physical_id: "my-rs".to_string(),
resource_type: "AWS::SES::ReceiptRuleSet".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: BTreeMap::new(),
};
assert_eq!(prov.get_att(&fresh, "RuleSetName"), None);
}
#[test]
fn ses_receipt_rule_lifecycle() {
let prov = make_provisioner();
let rs = make_resource(
"AWS::SES::ReceiptRuleSet",
"MyRuleSet",
serde_json::json!({"RuleSetName": "my-rs2"}),
);
prov.create_resource(&rs).unwrap();
let res = make_resource(
"AWS::SES::ReceiptRule",
"MyRule",
serde_json::json!({
"RuleSetName": "my-rs2",
"Rule": {
"Name": "rule1",
"Priority": 1,
"Enabled": true,
"Actions": [{"S3Action": {"BucketName": "my-bucket"}}],
},
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "my-rs2|rule1");
prov.delete_resource(&sr.clone()).unwrap();
}
#[test]
fn ses_receipt_filter_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SES::ReceiptFilter",
"MyFilter",
serde_json::json!({
"Filter": {
"Name": "my-filter",
"IpFilter": {"Policy": "Block", "Cidr": "10.0.0.0/8"},
},
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "my-filter");
prov.delete_resource(&sr.clone()).unwrap();
}
#[test]
fn ses_vdm_attributes_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::SES::VdmAttributes",
"MyVdm",
serde_json::json!({
"DashboardAttributes": {"EngagementMetrics": "ENABLED"},
"GuardianAttributes": {"OptimizedSharedDelivery": "ENABLED"},
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "vdm-MyVdm");
prov.delete_resource(&sr.clone()).unwrap();
}
#[test]
fn athena_work_group_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::Athena::WorkGroup",
"MyWg",
serde_json::json!({
"Name": "my-wg",
"Description": "test wg",
"Configuration": {"EnforceWorkGroupConfiguration": true},
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "my-wg");
assert_eq!(sr.attributes.get("Name"), Some(&"my-wg".to_string()));
assert!(sr
.attributes
.get("Arn")
.unwrap()
.contains("workgroup/my-wg"));
assert_eq!(
prov.get_att(
&StackResource {
resource_type: "AWS::Athena::WorkGroup".to_string(),
physical_id: sr.physical_id.clone(),
logical_id: "MyWg".to_string(),
status: "CREATE_COMPLETE".to_string(),
service_token: None,
attributes: BTreeMap::new(),
},
"Name",
),
Some("my-wg".to_string()),
);
prov.delete_resource(&sr.clone()).unwrap();
}
#[test]
fn athena_data_catalog_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::Athena::DataCatalog",
"MyCatalog",
serde_json::json!({
"Name": "my-catalog",
"Type": "GLUE",
"Description": "test catalog",
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "my-catalog");
assert_eq!(sr.attributes.get("Name"), Some(&"my-catalog".to_string()));
assert!(sr
.attributes
.get("Arn")
.unwrap()
.contains("datacatalog/my-catalog"));
prov.delete_resource(&sr.clone()).unwrap();
}
#[test]
fn athena_named_query_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::Athena::NamedQuery",
"MyQuery",
serde_json::json!({
"Name": "my-query",
"Database": "mydb",
"QueryString": "SELECT 1",
"WorkGroup": "primary",
}),
);
let sr = prov.create_resource(&res).unwrap();
assert!(!sr.physical_id.is_empty());
assert_eq!(sr.attributes.get("NamedQueryId"), Some(&sr.physical_id));
prov.delete_resource(&sr.clone()).unwrap();
}
#[test]
fn athena_prepared_statement_lifecycle() {
let prov = make_provisioner();
let res = make_resource(
"AWS::Athena::PreparedStatement",
"MyPs",
serde_json::json!({
"StatementName": "my-ps",
"WorkGroupName": "primary",
"QueryStatement": "SELECT 1",
}),
);
let sr = prov.create_resource(&res).unwrap();
assert_eq!(sr.physical_id, "primary|my-ps");
prov.delete_resource(&sr.clone()).unwrap();
}
}