use cargo_lambda_remote::{
aws_sdk_lambda::types::{Environment, TracingConfig},
RemoteConfig,
};
use clap::{ArgAction, Args, ValueHint};
use serde::{ser::SerializeStruct, Deserialize, Serialize};
use std::{collections::HashMap, fmt::Debug, path::PathBuf};
use strum_macros::{Display, EnumString};
use crate::{
cargo::deserialize_vec_or_map,
env::EnvOptions,
error::MetadataError,
lambda::{Memory, Timeout, Tracing},
};
const DEFAULT_MANIFEST_PATH: &str = "Cargo.toml";
const DEFAULT_COMPATIBLE_RUNTIMES: &str = "provided.al2,provided.al2023";
const DEFAULT_RUNTIME: &str = "provided.al2023";
#[derive(Args, Clone, Debug, Default, Deserialize)]
#[command(
name = "deploy",
after_help = "Full command documentation: https://www.cargo-lambda.info/commands/deploy.html"
)]
pub struct Deploy {
#[command(flatten)]
#[serde(flatten)]
pub remote_config: RemoteConfig,
#[command(flatten)]
#[serde(flatten)]
pub function_config: FunctionDeployConfig,
#[arg(short, long, value_hint = ValueHint::DirPath)]
#[serde(default)]
pub lambda_dir: Option<PathBuf>,
#[arg(long, value_name = "PATH", default_value = DEFAULT_MANIFEST_PATH)]
#[serde(default)]
pub manifest_path: Option<PathBuf>,
#[arg(long, conflicts_with = "binary_path")]
#[serde(default)]
pub binary_name: Option<String>,
#[arg(long, conflicts_with = "binary_name")]
#[serde(default)]
pub binary_path: Option<PathBuf>,
#[arg(long)]
#[serde(default)]
pub s3_bucket: Option<String>,
#[arg(long)]
#[serde(default)]
pub s3_key: Option<String>,
#[arg(long)]
#[serde(default)]
pub extension: bool,
#[arg(long, requires = "extension")]
#[serde(default)]
pub internal: bool,
#[arg(
long,
value_delimiter = ',',
default_value = DEFAULT_COMPATIBLE_RUNTIMES,
requires = "extension"
)]
#[serde(default)]
compatible_runtimes: Option<Vec<String>>,
#[arg(short, long)]
#[serde(default)]
output_format: Option<OutputFormat>,
#[arg(long, value_delimiter = ',', action = ArgAction::Append, visible_alias = "tags")]
#[serde(default, alias = "tags", deserialize_with = "deserialize_vec_or_map")]
pub tag: Option<Vec<String>>,
#[arg(short, long)]
#[serde(default)]
pub include: Option<Vec<String>>,
#[arg(long, alias = "dry-run")]
#[serde(default)]
pub dry: bool,
#[arg(value_name = "NAME")]
#[serde(default)]
pub name: Option<String>,
#[arg(skip)]
#[serde(skip)]
pub base_env: HashMap<String, String>,
}
impl Deploy {
pub fn manifest_path(&self) -> PathBuf {
self.manifest_path
.clone()
.unwrap_or_else(default_manifest_path)
}
pub fn output_format(&self) -> OutputFormat {
self.output_format.clone().unwrap_or_default()
}
pub fn compatible_runtimes(&self) -> Vec<String> {
self.compatible_runtimes
.clone()
.unwrap_or_else(default_compatible_runtimes)
}
pub fn tracing_config(&self) -> Option<TracingConfig> {
let tracing = self.function_config.tracing.clone()?;
Some(
TracingConfig::builder()
.mode(tracing.as_str().into())
.build(),
)
}
pub fn lambda_tags(&self) -> Option<HashMap<String, String>> {
match &self.tag {
None => None,
Some(tags) if tags.is_empty() => None,
Some(tags) => Some(extract_tags(tags)),
}
}
pub fn s3_tags(&self) -> Option<String> {
match &self.tag {
None => None,
Some(tags) if tags.is_empty() => None,
Some(tags) => Some(tags.join("&")),
}
}
pub fn lambda_environment(&self) -> Result<Option<Environment>, MetadataError> {
let builder = Environment::builder();
let env = match &self.function_config.env_options {
None => self.base_env.clone(),
Some(env_options) => env_options.lambda_environment(&self.base_env)?,
};
if env.is_empty() {
return Ok(None);
}
Ok(Some(builder.set_variables(Some(env)).build()))
}
pub fn publish_code_without_description(&self) -> bool {
self.function_config.description.is_none()
}
}
impl Serialize for Deploy {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let len = self.manifest_path.is_some() as usize
+ self.lambda_dir.is_some() as usize
+ self.binary_path.is_some() as usize
+ self.binary_name.is_some() as usize
+ self.s3_bucket.is_some() as usize
+ self.s3_key.is_some() as usize
+ self.extension as usize
+ self.internal as usize
+ self.compatible_runtimes.is_some() as usize
+ self.output_format.is_some() as usize
+ self.tag.is_some() as usize
+ self.include.is_some() as usize
+ self.dry as usize
+ self.name.is_some() as usize
+ self.remote_config.count_fields()
+ self.function_config.count_fields();
let mut state = serializer.serialize_struct("Deploy", len)?;
if let Some(ref path) = self.manifest_path {
state.serialize_field("manifest_path", path)?;
}
if let Some(ref dir) = self.lambda_dir {
state.serialize_field("lambda_dir", dir)?;
}
if let Some(ref path) = self.binary_path {
state.serialize_field("binary_path", path)?;
}
if let Some(ref name) = self.binary_name {
state.serialize_field("binary_name", name)?;
}
if let Some(ref bucket) = self.s3_bucket {
state.serialize_field("s3_bucket", bucket)?;
}
if let Some(ref key) = self.s3_key {
state.serialize_field("s3_key", key)?;
}
if self.extension {
state.serialize_field("extension", &self.extension)?;
}
if self.internal {
state.serialize_field("internal", &self.internal)?;
}
if let Some(ref runtimes) = self.compatible_runtimes {
state.serialize_field("compatible_runtimes", runtimes)?;
}
if let Some(ref format) = self.output_format {
state.serialize_field("output_format", format)?;
}
if let Some(ref tag) = self.tag {
state.serialize_field("tag", tag)?;
}
if let Some(ref include) = self.include {
state.serialize_field("include", include)?;
}
if self.dry {
state.serialize_field("dry", &self.dry)?;
}
if let Some(ref name) = self.name {
state.serialize_field("name", name)?;
}
self.remote_config.serialize_fields::<S>(&mut state)?;
self.function_config.serialize_fields::<S>(&mut state)?;
state.end()
}
}
fn default_manifest_path() -> PathBuf {
PathBuf::from(DEFAULT_MANIFEST_PATH)
}
fn default_compatible_runtimes() -> Vec<String> {
DEFAULT_COMPATIBLE_RUNTIMES
.split(',')
.map(String::from)
.collect()
}
#[derive(Clone, Debug, Default, Deserialize, Display, EnumString, Serialize)]
#[strum(ascii_case_insensitive)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
#[default]
Text,
Json,
}
#[derive(Args, Clone, Debug, Default, Deserialize, Serialize)]
pub struct FunctionDeployConfig {
#[arg(long)]
#[serde(default)]
pub enable_function_url: bool,
#[arg(long)]
#[serde(default)]
pub disable_function_url: bool,
#[arg(long, alias = "memory-size")]
#[serde(default)]
pub memory: Option<Memory>,
#[arg(long)]
#[serde(default)]
pub timeout: Option<Timeout>,
#[command(flatten)]
#[serde(flatten)]
pub env_options: Option<EnvOptions>,
#[arg(long)]
#[serde(default)]
pub tracing: Option<Tracing>,
#[arg(long, visible_alias = "iam-role")]
#[serde(default, alias = "iam_role")]
pub role: Option<String>,
#[arg(long, value_delimiter = ',', action = ArgAction::Append, visible_alias = "layer-arn")]
#[serde(default, alias = "layers")]
pub layer: Option<Vec<String>>,
#[command(flatten)]
#[serde(flatten)]
pub vpc: Option<VpcConfig>,
#[arg(long, default_value = DEFAULT_RUNTIME)]
#[serde(default)]
pub runtime: Option<String>,
#[arg(long)]
#[serde(default)]
pub description: Option<String>,
}
fn default_runtime() -> String {
DEFAULT_RUNTIME.to_string()
}
impl FunctionDeployConfig {
pub fn runtime(&self) -> String {
self.runtime.clone().unwrap_or_else(default_runtime)
}
pub fn should_update(&self) -> bool {
let Ok(val) = serde_json::to_value(self) else {
return false;
};
let Ok(default) = serde_json::to_value(FunctionDeployConfig::default()) else {
return false;
};
val != default
}
fn count_fields(&self) -> usize {
self.disable_function_url as usize
+ self.enable_function_url as usize
+ self.layer.as_ref().is_some_and(|l| !l.is_empty()) as usize
+ self.tracing.is_some() as usize
+ self.role.is_some() as usize
+ self.memory.is_some() as usize
+ self.timeout.is_some() as usize
+ self.runtime.is_some() as usize
+ self.description.is_some() as usize
+ self.vpc.as_ref().map_or(0, |vpc| vpc.count_fields())
+ self
.env_options
.as_ref()
.map_or(0, |env| env.count_fields())
}
fn serialize_fields<S>(
&self,
state: &mut <S as serde::Serializer>::SerializeStruct,
) -> Result<(), S::Error>
where
S: serde::Serializer,
{
if self.disable_function_url {
state.serialize_field("disable_function_url", &true)?;
}
if self.enable_function_url {
state.serialize_field("enable_function_url", &true)?;
}
if let Some(memory) = &self.memory {
state.serialize_field("memory", &memory)?;
}
if let Some(timeout) = &self.timeout {
state.serialize_field("timeout", &timeout)?;
}
if let Some(runtime) = &self.runtime {
state.serialize_field("runtime", &runtime)?;
}
if let Some(tracing) = &self.tracing {
state.serialize_field("tracing", &tracing)?;
}
if let Some(role) = &self.role {
state.serialize_field("role", &role)?;
}
if let Some(layer) = &self.layer {
if !layer.is_empty() {
state.serialize_field("layer", &layer)?;
}
}
if let Some(description) = &self.description {
state.serialize_field("description", &description)?;
}
if let Some(vpc) = &self.vpc {
vpc.serialize_fields::<S>(state)?;
}
if let Some(env_options) = &self.env_options {
env_options.serialize_fields::<S>(state)?;
}
Ok(())
}
}
#[derive(Args, Clone, Debug, Default, Deserialize, Serialize)]
pub struct VpcConfig {
#[arg(long, value_delimiter = ',')]
#[serde(default)]
pub subnet_ids: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
#[serde(default)]
pub security_group_ids: Option<Vec<String>>,
#[arg(long)]
#[serde(default)]
pub ipv6_allowed_for_dual_stack: bool,
}
impl VpcConfig {
fn count_fields(&self) -> usize {
self.subnet_ids.is_some() as usize
+ self.security_group_ids.is_some() as usize
+ self.ipv6_allowed_for_dual_stack as usize
}
fn serialize_fields<S>(
&self,
state: &mut <S as serde::Serializer>::SerializeStruct,
) -> Result<(), S::Error>
where
S: serde::Serializer,
{
if let Some(subnet_ids) = &self.subnet_ids {
state.serialize_field("subnet_ids", &subnet_ids)?;
}
if let Some(security_group_ids) = &self.security_group_ids {
state.serialize_field("security_group_ids", &security_group_ids)?;
}
state.serialize_field(
"ipv6_allowed_for_dual_stack",
&self.ipv6_allowed_for_dual_stack,
)?;
Ok(())
}
pub fn should_update(&self) -> bool {
let Ok(val) = serde_json::to_value(self) else {
return false;
};
let Ok(default) = serde_json::to_value(VpcConfig::default()) else {
return false;
};
val != default
}
}
fn extract_tags(tags: &Vec<String>) -> HashMap<String, String> {
let mut map = HashMap::new();
for var in tags {
let mut split = var.splitn(2, '=');
if let (Some(k), Some(v)) = (split.next(), split.next()) {
map.insert(k.to_string(), v.to_string());
}
}
map
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_tags() {
let tags = vec!["organization=aws".to_string(), "team=lambda".to_string()];
let map = extract_tags(&tags);
assert_eq!(map.get("organization"), Some(&"aws".to_string()));
assert_eq!(map.get("team"), Some(&"lambda".to_string()));
}
#[test]
fn test_lambda_environment() {
let deploy = Deploy::default();
let env = deploy.lambda_environment().unwrap();
assert_eq!(env, None);
let deploy = Deploy {
base_env: HashMap::from([("FOO".to_string(), "BAR".to_string())]),
..Default::default()
};
let env = deploy.lambda_environment().unwrap().unwrap();
assert_eq!(env.variables().unwrap().len(), 1);
assert_eq!(
env.variables().unwrap().get("FOO"),
Some(&"BAR".to_string())
);
let deploy = Deploy {
function_config: FunctionDeployConfig {
env_options: Some(EnvOptions {
env_var: Some(vec!["FOO=BAR".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
};
let env = deploy.lambda_environment().unwrap().unwrap();
assert_eq!(env.variables().unwrap().len(), 1);
assert_eq!(
env.variables().unwrap().get("FOO"),
Some(&"BAR".to_string())
);
let deploy = Deploy {
function_config: FunctionDeployConfig {
env_options: Some(EnvOptions {
env_var: Some(vec!["FOO=BAR".to_string()]),
..Default::default()
}),
..Default::default()
},
base_env: HashMap::from([("BAZ".to_string(), "QUX".to_string())]),
..Default::default()
};
let env = deploy.lambda_environment().unwrap().unwrap();
assert_eq!(env.variables().unwrap().len(), 2);
assert_eq!(
env.variables().unwrap().get("BAZ"),
Some(&"QUX".to_string())
);
assert_eq!(
env.variables().unwrap().get("FOO"),
Some(&"BAR".to_string())
);
let temp_file = tempfile::NamedTempFile::new().unwrap();
let path = temp_file.path();
std::fs::write(path, "FOO=BAR\nBAZ=QUX").unwrap();
let deploy = Deploy {
function_config: FunctionDeployConfig {
env_options: Some(EnvOptions {
env_file: Some(path.to_path_buf()),
..Default::default()
}),
..Default::default()
},
base_env: HashMap::from([("QUUX".to_string(), "QUUX".to_string())]),
..Default::default()
};
let env = deploy.lambda_environment().unwrap().unwrap();
assert_eq!(env.variables().unwrap().len(), 3);
assert_eq!(
env.variables().unwrap().get("BAZ"),
Some(&"QUX".to_string())
);
assert_eq!(
env.variables().unwrap().get("FOO"),
Some(&"BAR".to_string())
);
assert_eq!(
env.variables().unwrap().get("QUUX"),
Some(&"QUUX".to_string())
);
}
}