use super::*;
impl ResourceProvisioner {
pub(super) 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,
}
}
pub(super) 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,
durable_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,
};
if let Some(runtime) = self.lambda_runtime.clone() {
if let Ok(handle) = tokio::runtime::Handle::try_current() {
let func_for_prepull = func.clone();
let name = function_name.clone();
handle.spawn(async move {
match runtime.prepull_for_function(&func_for_prepull).await {
Some(Ok(())) => {
tracing::info!(function = %name, "pre-pulled CFN Lambda runtime image");
}
Some(Err(e)) => {
tracing::warn!(
function = %name,
error = %e,
"CFN Lambda runtime image pre-pull failed; Invoke will retry on cold path"
);
}
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"))
}
pub(super) 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"))
}
pub(super) 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(())
}
pub(super) 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))
}
pub(super) 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()))
}
pub(super) 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(())
}
pub(super) 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,
source_access_configurations: Vec::new(),
};
state.event_source_mappings.insert(uuid.clone(), esm);
Ok(ProvisionResult::new(uuid.clone()).with("Id", uuid))
}
pub(super) 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()))
}
pub(super) 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(())
}
pub(super) 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))
}
pub(super) 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(())
}
pub(super) 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))
}
pub(super) 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))
}
pub(super) 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(())
}
pub(super) 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(alias_arn.clone()).with("AliasArn", alias_arn))
}
pub(super) 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 key = alias_state_key(&existing.physical_id);
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
let alias = state.aliases.get_mut(&key).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(
key.clone(),
fakecloud_lambda::ProvisionedConcurrencyConfig {
requested: cnt,
allocated: cnt,
status: "READY".to_string(),
last_modified: Utc::now(),
},
);
}
None => {
state.provisioned_concurrency.remove(&key);
}
}
Ok(ProvisionResult::new(existing.physical_id.clone()).with("AliasArn", alias_arn))
}
pub(super) fn delete_lambda_alias(&self, physical_id: &str) -> Result<(), String> {
let key = alias_state_key(physical_id);
let mut accounts = self.lambda_state.write();
let state = accounts.get_or_create(&self.account_id);
state.aliases.remove(&key);
state.provisioned_concurrency.remove(&key);
Ok(())
}
pub(super) 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))
}
pub(super) 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))
}
pub(super) 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))
}
pub(super) 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(())
}
}