use awsim_core::{AwsError, RequestContext};
use serde_json::{Value, json};
use crate::{
error::{invalid_parameter, resource_not_found},
operations::functions::persist_code,
state::{FunctionVersion, LambdaState},
util::{now_iso8601, opt_str, require_str},
};
pub fn publish_version(
state: &LambdaState,
input: &Value,
_ctx: &RequestContext,
) -> Result<Value, AwsError> {
let name = require_str(input, "FunctionName")?;
let description = opt_str(input, "Description").unwrap_or("").to_string();
let expected_sha256 = opt_str(input, "CodeSha256");
let current_bytes = {
let f = state
.functions
.get(name)
.ok_or_else(|| resource_not_found("function", name))?;
if let Some(expected) = expected_sha256
&& f.code_sha256 != expected
{
return Err(invalid_parameter(format!(
"CodeSha256 ({expected}) does not match the function's current code SHA-256 \
({})",
f.code_sha256
)));
}
f.code
.as_ref()
.map(|c| c.read_all())
.transpose()
.map_err(|e| AwsError::internal(format!("read function code: {e}")))?
};
let mut f = state
.functions
.get_mut(name)
.ok_or_else(|| resource_not_found("function", name))?;
let next_version: u64 = f
.versions
.iter()
.filter_map(|v| v.version.parse::<u64>().ok())
.max()
.unwrap_or(0)
+ 1;
let version_number = next_version.to_string();
let now = now_iso8601();
let version_code = persist_code(state, name, &version_number, current_bytes)?;
let ver = FunctionVersion {
version: version_number.clone(),
description: description.clone(),
code_sha256: f.code_sha256.clone(),
code_size: f.code_size,
code: version_code,
last_modified: now.clone(),
};
f.versions.push(ver);
Ok(json!({
"FunctionName": f.name,
"FunctionArn": format!("{}:{}", f.arn, version_number),
"Runtime": f.runtime,
"Role": f.role,
"Handler": f.handler,
"Description": description,
"Timeout": f.timeout,
"MemorySize": f.memory_size,
"CodeSha256": f.code_sha256,
"CodeSize": f.code_size,
"Version": version_number,
"LastModified": now,
"State": "Active",
}))
}
pub fn list_versions_by_function(
state: &LambdaState,
input: &Value,
_ctx: &RequestContext,
) -> Result<Value, AwsError> {
use awsim_core::pagination::{cap_max_results, paginate};
let name = require_str(input, "FunctionName")?;
let f = state
.functions
.get(name)
.ok_or_else(|| resource_not_found("function", name))?;
let mut entries: Vec<Value> = vec![json!({
"FunctionName": f.name,
"FunctionArn": f.arn,
"Runtime": f.runtime,
"Role": f.role,
"Handler": f.handler,
"Description": f.description,
"Timeout": f.timeout,
"MemorySize": f.memory_size,
"CodeSha256": f.code_sha256,
"CodeSize": f.code_size,
"Version": "$LATEST",
"LastModified": f.last_modified,
"State": f.state,
})];
for ver in &f.versions {
entries.push(json!({
"FunctionName": f.name,
"FunctionArn": format!("{}:{}", f.arn, ver.version),
"Runtime": f.runtime,
"Role": f.role,
"Handler": f.handler,
"Description": ver.description,
"Timeout": f.timeout,
"MemorySize": f.memory_size,
"CodeSha256": ver.code_sha256,
"CodeSize": ver.code_size,
"Version": ver.version,
"LastModified": ver.last_modified,
"State": "Active",
}));
}
let max = cap_max_results(input.get("MaxItems").and_then(Value::as_i64), 50, 50);
let marker = input.get("Marker").and_then(Value::as_str);
let page = paginate(entries, max, marker, |v| {
v["Version"].as_str().unwrap_or("").to_string()
})?;
let mut result = json!({ "Versions": page.items });
if let Some(token) = page.next_token {
result["NextMarker"] = json!(token);
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::operations::functions::create_function;
use crate::state::LambdaState;
fn ctx() -> RequestContext {
RequestContext::new("lambda", "us-east-1")
}
fn empty_zip_b64() -> String {
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
let bytes: [u8; 22] = [
0x50, 0x4b, 0x05, 0x06, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
BASE64.encode(bytes)
}
fn create_test_fn(state: &LambdaState) {
create_function(
state,
&json!({
"FunctionName": "f",
"Role": "arn:aws:iam::000000000000:role/test",
"Code": { "ZipFile": empty_zip_b64() },
}),
&ctx(),
)
.unwrap();
}
#[test]
fn publish_version_starts_at_one() {
let state = LambdaState::default();
create_test_fn(&state);
let resp = publish_version(&state, &json!({ "FunctionName": "f" }), &ctx()).unwrap();
assert_eq!(resp["Version"], json!("1"));
}
#[test]
fn publish_version_rejects_codesha256_mismatch() {
let state = LambdaState::default();
create_test_fn(&state);
let err = publish_version(
&state,
&json!({
"FunctionName": "f",
"CodeSha256": "definitely-not-the-current-hash",
}),
&ctx(),
)
.unwrap_err();
assert_eq!(err.code, "InvalidParameterValueException");
assert!(err.message.contains("CodeSha256"));
}
#[test]
fn publish_version_accepts_matching_codesha256() {
let state = LambdaState::default();
create_test_fn(&state);
let current_hash = state.functions.get("f").unwrap().code_sha256.clone();
let resp = publish_version(
&state,
&json!({
"FunctionName": "f",
"CodeSha256": current_hash,
}),
&ctx(),
)
.unwrap();
assert_eq!(resp["Version"], json!("1"));
}
#[test]
fn publish_version_after_delete_does_not_reuse_number() {
let state = LambdaState::default();
create_test_fn(&state);
publish_version(&state, &json!({ "FunctionName": "f" }), &ctx()).unwrap();
publish_version(&state, &json!({ "FunctionName": "f" }), &ctx()).unwrap();
{
let mut f = state.functions.get_mut("f").unwrap();
f.versions.retain(|v| v.version != "1");
}
let resp = publish_version(&state, &json!({ "FunctionName": "f" }), &ctx()).unwrap();
assert_eq!(resp["Version"], json!("3"));
}
}