Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@ name: Build Crates
on:
pull_request:
paths:
- 'crates/**'
- 'tests/**'
- 'Cargo.toml'
- 'deny.toml'
- 'rustfmt.toml'
- '.github/workflows/build.yml'
- "crates/**"
- "tests/**"
- "Cargo.toml"
- "deny.toml"
- "rustfmt.toml"
- ".github/workflows/build.yml"

push:
branches: [ 'main' ]
tags-ignore: [ '*' ]
branches: ["main"]
tags-ignore: ["*"]
paths:
- 'crates/**'
- 'tests/**'
- 'Cargo.toml'
- 'deny.toml'
- 'rustfmt.toml'
- '.github/workflows/build.yml'
- "crates/**"
- "tests/**"
- "Cargo.toml"
- "deny.toml"
- "rustfmt.toml"
- ".github/workflows/build.yml"

workflow_dispatch:

Expand All @@ -37,7 +37,7 @@ jobs:
matrix:
os:
- label: MacOS (Intel)
runner: macos-13
runner: macos-15-intel
- label: MacOS (ARM)
runner: macos-latest
- label: Ubuntu Latest
Expand All @@ -63,7 +63,7 @@ jobs:
- name: Setup cmake
uses: jwlawson/actions-setup-cmake@v2
with:
cmake-version: '3.31.x'
cmake-version: "3.31.x"

- name: Run cargo test
run: cargo test --all-features
17 changes: 9 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Release

on:
push:
tags: [ 'v*' ]
tags: ["v*"]

env:
MATURIN_VERSION: 1.8.2
Expand All @@ -22,7 +22,7 @@ jobs:
os: macos-latest
label: MacOs (ARM)
- target: x86_64-apple-darwin
os: macos-13
os: macos-15-intel
label: MacOS (Intel)
steps:
- uses: actions/checkout@v5
Expand Down Expand Up @@ -67,7 +67,7 @@ jobs:
- name: Setup cmake
uses: jwlawson/actions-setup-cmake@v2
with:
cmake-version: '3.31.x'
cmake-version: "3.31.x"

- name: Build
uses: messense/maturin-action@v1
Expand All @@ -92,10 +92,11 @@ jobs:
strategy:
fail-fast: true
matrix:
platform: [
{ target: "x86_64-unknown-linux-musl", image_tag: "x86_64-musl" },
{ target: "aarch64-unknown-linux-musl", image_tag: "aarch64-musl" },
]
platform:
[
{ target: "x86_64-unknown-linux-musl", image_tag: "x86_64-musl" },
{ target: "aarch64-unknown-linux-musl", image_tag: "aarch64-musl" },
]
container:
image: ghcr.io/rust-cross/rust-musl-cross:${{ matrix.platform.image_tag }}
steps:
Expand Down Expand Up @@ -128,7 +129,7 @@ jobs:
release-all:
name: Release all artifacts
runs-on: ubuntu-latest
needs: [ release-macos, release-windows, release-linux ]
needs: [release-macos, release-windows, release-linux]
steps:
- uses: actions/checkout@v5
- uses: actions/download-artifact@v4
Expand Down
98 changes: 97 additions & 1 deletion crates/cargo-lambda-deploy/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,19 @@ async fn update_function_config(
builder = builder.set_layers(config.function_config.layer.clone());
}

if let Some(environment) = config.lambda_environment()? {
// If merge_env is enabled, populate remote_env with existing environment variables
let config_with_remote_env = if config.merge_env {
let mut config_clone = config.clone();
config_clone.remote_env = conf
.environment
.clone()
.and_then(|e| e.variables)
.unwrap_or_default();
config_clone
} else {
config.clone()
};
if let Some(environment) = config_with_remote_env.lambda_environment()? {
if let Some(vars) = environment.variables() {
if !vars.is_empty()
&& vars
Expand Down Expand Up @@ -1369,4 +1381,88 @@ mod tests {
assert!(result.is_ok());
http_client.assert_requests_match(&[]);
}

#[tokio::test]
async fn test_update_function_config_with_merge_env() {
use cargo_lambda_metadata::env::EnvOptions;
use std::collections::HashMap;

// Mock request body that should contain merged environment variables
let request_body = SdkBody::from(
serde_json::json!({
"Environment": {
"Variables": {
"REMOTE_VAR": "remote_value",
"LOCAL_VAR": "local_value",
"OVERRIDE_VAR": "overridden_value"
}
}
})
.to_string(),
);

let response_body = SdkBody::from(
serde_json::json!({
"FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:test-function",
"LastUpdateStatus": "Successful"
})
.to_string(),
);

let http_client = StaticReplayClient::new(vec![ReplayEvent::new(
Request::builder()
.uri("https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/test-function/configuration")
.method("PUT")
.body(request_body)
.unwrap(),
Response::builder().status(200).body(response_body).unwrap(),
)]);

let config = LambdaConfig::builder()
.http_client(http_client.clone())
.credentials_provider(Credentials::for_tests())
.region(Region::new("us-east-1"))
.build();
let client = LambdaClient::from_conf(config);

// Setup deploy config with merge_env enabled
let mut deploy_config = Deploy::default();
deploy_config.merge_env = true;
deploy_config.function_config.env_options = Some(EnvOptions {
env_var: Some(vec![
"LOCAL_VAR=local_value".to_string(),
"OVERRIDE_VAR=overridden_value".to_string(),
]),
env_file: None,
});

let name = "test-function";
let progress = Progress::start("deploying function");

// Create a function configuration with existing environment variables
let mut existing_env = HashMap::new();
existing_env.insert("REMOTE_VAR".to_string(), "remote_value".to_string());
existing_env.insert("OVERRIDE_VAR".to_string(), "old_value".to_string());

let environment =
cargo_lambda_remote::aws_sdk_lambda::types::EnvironmentResponse::builder()
.set_variables(Some(existing_env))
.build();

let conf = FunctionConfiguration::builder()
.function_arn("arn:aws:lambda:us-east-1:123456789012:function:test-function")
.state(State::Active)
.last_update_status(LastUpdateStatus::Successful)
.environment(environment)
.build();

let result = update_function_config(&deploy_config, name, &client, &progress, conf).await;

assert!(result.is_ok());
assert_eq!(
result.unwrap(),
"arn:aws:lambda:us-east-1:123456789012:function:test-function"
);
http_client.assert_requests_match(&[]);
}
}
109 changes: 106 additions & 3 deletions crates/cargo-lambda-metadata/src/cargo/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ pub struct Deploy {
#[serde(default)]
pub dry: bool,

/// Merge environment variables with existing ones instead of overwriting them.
/// When enabled, existing environment variables on AWS Lambda are preserved,
/// and only new variables are added or updated from the configuration.
#[arg(long)]
#[serde(default)]
pub merge_env: bool,

/// Name of the function or extension to deploy
#[arg(value_name = "NAME")]
#[serde(default)]
Expand All @@ -118,6 +125,10 @@ pub struct Deploy {
#[arg(skip)]
#[serde(skip)]
pub base_env: HashMap<String, String>,

#[arg(skip)]
#[serde(skip)]
pub remote_env: HashMap<String, String>,
}

impl Deploy {
Expand Down Expand Up @@ -180,9 +191,23 @@ impl Deploy {
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)?,
let mut env = if self.merge_env {
// Start with remote environment variables when merging
self.remote_env.clone()
} else {
HashMap::new()
};

// Add base environment variables
env.extend(self.base_env.clone());

// Add or override with environment variables from configuration
match &self.function_config.env_options {
None => {}
Some(env_options) => {
let local_env = env_options.lambda_environment(&HashMap::new())?;
env.extend(local_env);
}
};

if env.is_empty() {
Expand Down Expand Up @@ -221,6 +246,7 @@ impl Serialize for Deploy {
+ self.tag.is_some() as usize
+ self.include.is_some() as usize
+ self.dry as usize
+ self.merge_env as usize
+ self.name.is_some() as usize
+ self.remote_config.is_some() as usize
+ self.function_config.count_fields();
Expand Down Expand Up @@ -266,6 +292,9 @@ impl Serialize for Deploy {
if self.dry {
state.serialize_field("dry", &self.dry)?;
}
if self.merge_env {
state.serialize_field("merge_env", &self.merge_env)?;
}
if let Some(ref name) = self.name {
state.serialize_field("name", name)?;
}
Expand Down Expand Up @@ -632,6 +661,80 @@ mod tests {
);
}

#[test]
fn test_lambda_environment_merge_mode() {
// Test merge_env=false (default behavior - overwrites)
let deploy = Deploy {
function_config: FunctionDeployConfig {
env_options: Some(EnvOptions {
env_var: Some(vec!["LOCAL=VALUE".to_string()]),
..Default::default()
}),
..Default::default()
},
remote_env: HashMap::from([
("REMOTE1".to_string(), "REMOTE_VALUE1".to_string()),
("REMOTE2".to_string(), "REMOTE_VALUE2".to_string()),
]),
merge_env: false,
..Default::default()
};
let env = deploy.lambda_environment().unwrap().unwrap();
let vars = env.variables().unwrap();
// When merge_env is false, only local env vars should be present
assert_eq!(vars.len(), 1);
assert_eq!(vars.get("LOCAL"), Some(&"VALUE".to_string()));
assert_eq!(vars.get("REMOTE1"), None);
assert_eq!(vars.get("REMOTE2"), None);

// Test merge_env=true (merge behavior - preserves remote vars)
let deploy = Deploy {
function_config: FunctionDeployConfig {
env_options: Some(EnvOptions {
env_var: Some(vec!["LOCAL=VALUE".to_string()]),
..Default::default()
}),
..Default::default()
},
remote_env: HashMap::from([
("REMOTE1".to_string(), "REMOTE_VALUE1".to_string()),
("REMOTE2".to_string(), "REMOTE_VALUE2".to_string()),
]),
merge_env: true,
..Default::default()
};
let env = deploy.lambda_environment().unwrap().unwrap();
let vars = env.variables().unwrap();
// When merge_env is true, both remote and local vars should be present
assert_eq!(vars.len(), 3);
assert_eq!(vars.get("LOCAL"), Some(&"VALUE".to_string()));
assert_eq!(vars.get("REMOTE1"), Some(&"REMOTE_VALUE1".to_string()));
assert_eq!(vars.get("REMOTE2"), Some(&"REMOTE_VALUE2".to_string()));

// Test merge_env=true with overlapping keys (local should win)
let deploy = Deploy {
function_config: FunctionDeployConfig {
env_options: Some(EnvOptions {
env_var: Some(vec!["REMOTE1=LOCAL_OVERRIDE".to_string()]),
..Default::default()
}),
..Default::default()
},
remote_env: HashMap::from([
("REMOTE1".to_string(), "REMOTE_VALUE1".to_string()),
("REMOTE2".to_string(), "REMOTE_VALUE2".to_string()),
]),
merge_env: true,
..Default::default()
};
let env = deploy.lambda_environment().unwrap().unwrap();
let vars = env.variables().unwrap();
// Local value should override remote value
assert_eq!(vars.len(), 2);
assert_eq!(vars.get("REMOTE1"), Some(&"LOCAL_OVERRIDE".to_string()));
assert_eq!(vars.get("REMOTE2"), Some(&"REMOTE_VALUE2".to_string()));
}

#[test]
fn test_load_config_from_workspace() {
let options = ConfigOptions {
Expand Down
Loading