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