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