1use cargo_lambda_remote::{
2 RemoteConfig,
3 aws_sdk_lambda::types::{Environment, TracingConfig},
4};
5use clap::{ArgAction, Args, ValueHint};
6use serde::{Deserialize, Serialize, ser::SerializeStruct};
7use std::{collections::HashMap, fmt::Debug, path::PathBuf};
8use strum_macros::{Display, EnumString};
9
10use crate::{
11 cargo::deserialize_vec_or_map,
12 env::EnvOptions,
13 error::MetadataError,
14 lambda::{Memory, Timeout, Tracing},
15};
16
17const DEFAULT_MANIFEST_PATH: &str = "Cargo.toml";
18const DEFAULT_COMPATIBLE_RUNTIMES: &str = "provided.al2,provided.al2023";
19const DEFAULT_RUNTIME: &str = "provided.al2023";
20
21#[derive(Args, Clone, Debug, Default, Deserialize)]
22#[command(
23 name = "deploy",
24 after_help = "Full command documentation: https://www.cargo-lambda.info/commands/deploy.html"
25)]
26pub struct Deploy {
27 #[command(flatten)]
28 #[serde(flatten)]
29 pub remote_config: RemoteConfig,
30
31 #[command(flatten)]
32 #[serde(flatten)]
33 pub function_config: FunctionDeployConfig,
34
35 #[arg(short, long, value_hint = ValueHint::DirPath)]
37 #[serde(default)]
38 pub lambda_dir: Option<PathBuf>,
39
40 #[arg(long, value_name = "PATH", default_value = DEFAULT_MANIFEST_PATH)]
42 #[serde(default)]
43 pub manifest_path: Option<PathBuf>,
44
45 #[arg(long, conflicts_with = "binary_path")]
47 #[serde(default)]
48 pub binary_name: Option<String>,
49
50 #[arg(long, conflicts_with = "binary_name")]
52 #[serde(default)]
53 pub binary_path: Option<PathBuf>,
54
55 #[arg(long)]
57 #[serde(default)]
58 pub s3_bucket: Option<String>,
59
60 #[arg(long)]
62 #[serde(default)]
63 pub s3_key: Option<String>,
64
65 #[arg(long)]
67 #[serde(default)]
68 pub extension: bool,
69
70 #[arg(long, requires = "extension")]
72 #[serde(default)]
73 pub internal: bool,
74
75 #[arg(
78 long,
79 value_delimiter = ',',
80 default_value = DEFAULT_COMPATIBLE_RUNTIMES,
81 requires = "extension"
82 )]
83 #[serde(default)]
84 compatible_runtimes: Option<Vec<String>>,
85
86 #[arg(short, long)]
88 #[serde(default)]
89 output_format: Option<OutputFormat>,
90
91 #[arg(long, value_delimiter = ',', action = ArgAction::Append, visible_alias = "tags")]
94 #[serde(default, alias = "tags", deserialize_with = "deserialize_vec_or_map")]
95 pub tag: Option<Vec<String>>,
96
97 #[arg(short, long)]
99 #[serde(default)]
100 pub include: Option<Vec<String>>,
101
102 #[arg(long, alias = "dry-run")]
104 #[serde(default)]
105 pub dry: bool,
106
107 #[arg(value_name = "NAME")]
109 #[serde(default)]
110 pub name: Option<String>,
111
112 #[arg(skip)]
113 #[serde(skip)]
114 pub base_env: HashMap<String, String>,
115}
116
117impl Deploy {
118 pub fn manifest_path(&self) -> PathBuf {
119 self.manifest_path
120 .clone()
121 .unwrap_or_else(default_manifest_path)
122 }
123
124 pub fn output_format(&self) -> OutputFormat {
125 self.output_format.clone().unwrap_or_default()
126 }
127
128 pub fn compatible_runtimes(&self) -> Vec<String> {
129 self.compatible_runtimes
130 .clone()
131 .unwrap_or_else(default_compatible_runtimes)
132 }
133
134 pub fn tracing_config(&self) -> Option<TracingConfig> {
135 let tracing = self.function_config.tracing.clone()?;
136
137 Some(
138 TracingConfig::builder()
139 .mode(tracing.as_str().into())
140 .build(),
141 )
142 }
143
144 pub fn lambda_tags(&self) -> Option<HashMap<String, String>> {
145 match &self.tag {
146 None => None,
147 Some(tags) if tags.is_empty() => None,
148 Some(tags) => Some(extract_tags(tags)),
149 }
150 }
151
152 pub fn s3_tags(&self) -> Option<String> {
153 match &self.tag {
154 None => None,
155 Some(tags) if tags.is_empty() => None,
156 Some(tags) => Some(tags.join("&")),
157 }
158 }
159
160 pub fn lambda_environment(&self) -> Result<Option<Environment>, MetadataError> {
161 let builder = Environment::builder();
162
163 let env = match &self.function_config.env_options {
164 None => self.base_env.clone(),
165 Some(env_options) => env_options.lambda_environment(&self.base_env)?,
166 };
167
168 if env.is_empty() {
169 return Ok(None);
170 }
171
172 Ok(Some(builder.set_variables(Some(env)).build()))
173 }
174
175 pub fn publish_code_without_description(&self) -> bool {
176 self.function_config.description.is_none()
177 }
178}
179
180impl Serialize for Deploy {
181 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
182 where
183 S: serde::Serializer,
184 {
185 use serde::ser::SerializeStruct;
186
187 let len = self.manifest_path.is_some() as usize
188 + self.lambda_dir.is_some() as usize
189 + self.binary_path.is_some() as usize
190 + self.binary_name.is_some() as usize
191 + self.s3_bucket.is_some() as usize
192 + self.s3_key.is_some() as usize
193 + self.extension as usize
194 + self.internal as usize
195 + self.compatible_runtimes.is_some() as usize
196 + self.output_format.is_some() as usize
197 + self.tag.is_some() as usize
198 + self.include.is_some() as usize
199 + self.dry as usize
200 + self.name.is_some() as usize
201 + self.remote_config.count_fields()
202 + self.function_config.count_fields();
203
204 let mut state = serializer.serialize_struct("Deploy", len)?;
205
206 if let Some(ref path) = self.manifest_path {
207 state.serialize_field("manifest_path", path)?;
208 }
209 if let Some(ref dir) = self.lambda_dir {
210 state.serialize_field("lambda_dir", dir)?;
211 }
212 if let Some(ref path) = self.binary_path {
213 state.serialize_field("binary_path", path)?;
214 }
215 if let Some(ref name) = self.binary_name {
216 state.serialize_field("binary_name", name)?;
217 }
218 if let Some(ref bucket) = self.s3_bucket {
219 state.serialize_field("s3_bucket", bucket)?;
220 }
221 if let Some(ref key) = self.s3_key {
222 state.serialize_field("s3_key", key)?;
223 }
224 if self.extension {
225 state.serialize_field("extension", &self.extension)?;
226 }
227 if self.internal {
228 state.serialize_field("internal", &self.internal)?;
229 }
230 if let Some(ref runtimes) = self.compatible_runtimes {
231 state.serialize_field("compatible_runtimes", runtimes)?;
232 }
233 if let Some(ref format) = self.output_format {
234 state.serialize_field("output_format", format)?;
235 }
236 if let Some(ref tag) = self.tag {
237 state.serialize_field("tag", tag)?;
238 }
239 if let Some(ref include) = self.include {
240 state.serialize_field("include", include)?;
241 }
242 if self.dry {
243 state.serialize_field("dry", &self.dry)?;
244 }
245 if let Some(ref name) = self.name {
246 state.serialize_field("name", name)?;
247 }
248
249 self.remote_config.serialize_fields::<S>(&mut state)?;
250 self.function_config.serialize_fields::<S>(&mut state)?;
251
252 state.end()
253 }
254}
255
256fn default_manifest_path() -> PathBuf {
257 PathBuf::from(DEFAULT_MANIFEST_PATH)
258}
259
260fn default_compatible_runtimes() -> Vec<String> {
261 DEFAULT_COMPATIBLE_RUNTIMES
262 .split(',')
263 .map(String::from)
264 .collect()
265}
266
267#[derive(Clone, Debug, Default, Deserialize, Display, EnumString, Serialize)]
268#[strum(ascii_case_insensitive)]
269#[serde(rename_all = "lowercase")]
270pub enum OutputFormat {
271 #[default]
272 Text,
273 Json,
274}
275
276#[derive(Args, Clone, Debug, Default, Deserialize, Serialize)]
277pub struct FunctionDeployConfig {
278 #[arg(long)]
280 #[serde(default)]
281 pub enable_function_url: bool,
282
283 #[arg(long)]
285 #[serde(default)]
286 pub disable_function_url: bool,
287
288 #[arg(long, alias = "memory-size", value_parser = clap::value_parser!(i32).range(128..=10240))]
290 #[serde(default)]
291 pub memory: Option<Memory>,
292
293 #[arg(long)]
295 #[serde(default)]
296 pub timeout: Option<Timeout>,
297
298 #[command(flatten)]
299 #[serde(flatten)]
300 pub env_options: Option<EnvOptions>,
301
302 #[arg(long)]
304 #[serde(default)]
305 pub tracing: Option<Tracing>,
306
307 #[arg(long, visible_alias = "iam-role")]
309 #[serde(default, alias = "iam_role")]
310 pub role: Option<String>,
311
312 #[arg(long, value_delimiter = ',', action = ArgAction::Append, visible_alias = "layer-arn")]
318 #[serde(default, alias = "layers")]
319 pub layer: Option<Vec<String>>,
320
321 #[command(flatten)]
322 #[serde(flatten)]
323 pub vpc: Option<VpcConfig>,
324
325 #[arg(long, default_value = DEFAULT_RUNTIME)]
328 #[serde(default)]
329 pub runtime: Option<String>,
330
331 #[arg(long)]
333 #[serde(default)]
334 pub description: Option<String>,
335}
336
337fn default_runtime() -> String {
338 DEFAULT_RUNTIME.to_string()
339}
340
341impl FunctionDeployConfig {
342 pub fn runtime(&self) -> String {
343 self.runtime.clone().unwrap_or_else(default_runtime)
344 }
345
346 pub fn should_update(&self) -> bool {
347 let Ok(val) = serde_json::to_value(self) else {
348 return false;
349 };
350 let Ok(default) = serde_json::to_value(FunctionDeployConfig::default()) else {
351 return false;
352 };
353 val != default
354 }
355
356 fn count_fields(&self) -> usize {
357 self.disable_function_url as usize
358 + self.enable_function_url as usize
359 + self.layer.as_ref().is_some_and(|l| !l.is_empty()) as usize
360 + self.tracing.is_some() as usize
361 + self.role.is_some() as usize
362 + self.memory.is_some() as usize
363 + self.timeout.is_some() as usize
364 + self.runtime.is_some() as usize
365 + self.description.is_some() as usize
366 + self.vpc.as_ref().map_or(0, |vpc| vpc.count_fields())
367 + self
368 .env_options
369 .as_ref()
370 .map_or(0, |env| env.count_fields())
371 }
372
373 fn serialize_fields<S>(
374 &self,
375 state: &mut <S as serde::Serializer>::SerializeStruct,
376 ) -> Result<(), S::Error>
377 where
378 S: serde::Serializer,
379 {
380 if self.disable_function_url {
381 state.serialize_field("disable_function_url", &true)?;
382 }
383
384 if self.enable_function_url {
385 state.serialize_field("enable_function_url", &true)?;
386 }
387
388 if let Some(memory) = &self.memory {
389 state.serialize_field("memory", &memory)?;
390 }
391
392 if let Some(timeout) = &self.timeout {
393 state.serialize_field("timeout", &timeout)?;
394 }
395
396 if let Some(runtime) = &self.runtime {
397 state.serialize_field("runtime", &runtime)?;
398 }
399
400 if let Some(tracing) = &self.tracing {
401 state.serialize_field("tracing", &tracing)?;
402 }
403
404 if let Some(role) = &self.role {
405 state.serialize_field("role", &role)?;
406 }
407
408 if let Some(layer) = &self.layer {
409 if !layer.is_empty() {
410 state.serialize_field("layer", &layer)?;
411 }
412 }
413
414 if let Some(description) = &self.description {
415 state.serialize_field("description", &description)?;
416 }
417
418 if let Some(vpc) = &self.vpc {
419 vpc.serialize_fields::<S>(state)?;
420 }
421
422 if let Some(env_options) = &self.env_options {
423 env_options.serialize_fields::<S>(state)?;
424 }
425
426 Ok(())
427 }
428}
429
430#[derive(Args, Clone, Debug, Default, Deserialize, Serialize)]
431pub struct VpcConfig {
432 #[arg(long, value_delimiter = ',')]
434 #[serde(default)]
435 pub subnet_ids: Option<Vec<String>>,
436
437 #[arg(long, value_delimiter = ',')]
439 #[serde(default)]
440 pub security_group_ids: Option<Vec<String>>,
441
442 #[arg(long)]
444 #[serde(default)]
445 pub ipv6_allowed_for_dual_stack: bool,
446}
447
448impl VpcConfig {
449 fn count_fields(&self) -> usize {
450 self.subnet_ids.is_some() as usize
451 + self.security_group_ids.is_some() as usize
452 + self.ipv6_allowed_for_dual_stack as usize
453 }
454
455 fn serialize_fields<S>(
456 &self,
457 state: &mut <S as serde::Serializer>::SerializeStruct,
458 ) -> Result<(), S::Error>
459 where
460 S: serde::Serializer,
461 {
462 if let Some(subnet_ids) = &self.subnet_ids {
463 state.serialize_field("subnet_ids", &subnet_ids)?;
464 }
465 if let Some(security_group_ids) = &self.security_group_ids {
466 state.serialize_field("security_group_ids", &security_group_ids)?;
467 }
468 state.serialize_field(
469 "ipv6_allowed_for_dual_stack",
470 &self.ipv6_allowed_for_dual_stack,
471 )?;
472 Ok(())
473 }
474
475 pub fn should_update(&self) -> bool {
476 let Ok(val) = serde_json::to_value(self) else {
477 return false;
478 };
479 let Ok(default) = serde_json::to_value(VpcConfig::default()) else {
480 return false;
481 };
482 val != default
483 }
484}
485
486fn extract_tags(tags: &Vec<String>) -> HashMap<String, String> {
487 let mut map = HashMap::new();
488
489 for var in tags {
490 let mut split = var.splitn(2, '=');
491 if let (Some(k), Some(v)) = (split.next(), split.next()) {
492 map.insert(k.to_string(), v.to_string());
493 }
494 }
495
496 map
497}
498
499#[cfg(test)]
500mod tests {
501 use crate::{
502 cargo::load_metadata,
503 config::{ConfigOptions, load_config_without_cli_flags},
504 lambda::{Memory, Timeout},
505 tests::fixture_metadata,
506 };
507
508 use super::*;
509
510 #[test]
511 fn test_extract_tags() {
512 let tags = vec!["organization=aws".to_string(), "team=lambda".to_string()];
513 let map = extract_tags(&tags);
514 assert_eq!(map.get("organization"), Some(&"aws".to_string()));
515 assert_eq!(map.get("team"), Some(&"lambda".to_string()));
516 }
517
518 #[test]
519 fn test_lambda_environment() {
520 let deploy = Deploy::default();
521 let env = deploy.lambda_environment().unwrap();
522 assert_eq!(env, None);
523
524 let deploy = Deploy {
525 base_env: HashMap::from([("FOO".to_string(), "BAR".to_string())]),
526 ..Default::default()
527 };
528 let env = deploy.lambda_environment().unwrap().unwrap();
529 assert_eq!(env.variables().unwrap().len(), 1);
530 assert_eq!(
531 env.variables().unwrap().get("FOO"),
532 Some(&"BAR".to_string())
533 );
534
535 let deploy = Deploy {
536 function_config: FunctionDeployConfig {
537 env_options: Some(EnvOptions {
538 env_var: Some(vec!["FOO=BAR".to_string()]),
539 ..Default::default()
540 }),
541 ..Default::default()
542 },
543 ..Default::default()
544 };
545 let env = deploy.lambda_environment().unwrap().unwrap();
546 assert_eq!(env.variables().unwrap().len(), 1);
547 assert_eq!(
548 env.variables().unwrap().get("FOO"),
549 Some(&"BAR".to_string())
550 );
551
552 let deploy = Deploy {
553 function_config: FunctionDeployConfig {
554 env_options: Some(EnvOptions {
555 env_var: Some(vec!["FOO=BAR".to_string()]),
556 ..Default::default()
557 }),
558 ..Default::default()
559 },
560 base_env: HashMap::from([("BAZ".to_string(), "QUX".to_string())]),
561 ..Default::default()
562 };
563 let env = deploy.lambda_environment().unwrap().unwrap();
564 assert_eq!(env.variables().unwrap().len(), 2);
565 assert_eq!(
566 env.variables().unwrap().get("BAZ"),
567 Some(&"QUX".to_string())
568 );
569 assert_eq!(
570 env.variables().unwrap().get("FOO"),
571 Some(&"BAR".to_string())
572 );
573
574 let temp_file = tempfile::NamedTempFile::new().unwrap();
575 let path = temp_file.path();
576 std::fs::write(path, "FOO=BAR\nBAZ=QUX").unwrap();
577
578 let deploy = Deploy {
579 function_config: FunctionDeployConfig {
580 env_options: Some(EnvOptions {
581 env_file: Some(path.to_path_buf()),
582 ..Default::default()
583 }),
584 ..Default::default()
585 },
586 base_env: HashMap::from([("QUUX".to_string(), "QUUX".to_string())]),
587 ..Default::default()
588 };
589 let env = deploy.lambda_environment().unwrap().unwrap();
590 assert_eq!(env.variables().unwrap().len(), 3);
591 assert_eq!(
592 env.variables().unwrap().get("BAZ"),
593 Some(&"QUX".to_string())
594 );
595 assert_eq!(
596 env.variables().unwrap().get("FOO"),
597 Some(&"BAR".to_string())
598 );
599 assert_eq!(
600 env.variables().unwrap().get("QUUX"),
601 Some(&"QUUX".to_string())
602 );
603 }
604
605 #[test]
606 fn test_load_config_from_workspace() {
607 let options = ConfigOptions {
608 name: Some("crate-3".to_string()),
609 admerge: true,
610 ..Default::default()
611 };
612
613 let metadata = load_metadata(fixture_metadata("workspace-package")).unwrap();
614 let config = load_config_without_cli_flags(&metadata, &options).unwrap();
615 assert_eq!(
616 config.deploy.function_config.timeout,
617 Some(Timeout::new(120))
618 );
619 assert_eq!(config.deploy.function_config.memory, Some(Memory(10240)));
620
621 let tags = config.deploy.lambda_tags().unwrap();
622 assert_eq!(tags.len(), 2);
623 assert_eq!(tags.get("organization"), Some(&"aws".to_string()));
624 assert_eq!(tags.get("team"), Some(&"lambda".to_string()));
625
626 assert_eq!(
627 config.deploy.include,
628 Some(vec!["src/bin/main.rs".to_string()])
629 );
630
631 assert_eq!(
632 config.deploy.function_config.env_options.unwrap().env_var,
633 Some(vec!["APP_ENV=production".to_string()])
634 );
635 }
636}