1use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use std::time::Duration;
9
10use async_trait::async_trait;
11use base64::Engine;
12use tempfile::TempDir;
13
14use super::backend::{BackendHandle, LambdaBackend, RuntimeError, WarmInstance};
15use super::env_rewrite::rewrite_localhost_envs;
16use crate::state::LambdaFunction;
17
18pub struct DockerBackend {
20 cli: String,
21 instance_id: String,
22 host_alias: String,
32 add_host_arg: Option<String>,
35 server_port: u16,
39 sibling_host: String,
49 docker_config: Option<Arc<TempDir>>,
53}
54
55impl DockerBackend {
56 pub fn auto_detect(server_port: u16) -> Option<Self> {
61 let cli = if let Ok(cli) = std::env::var("FAKECLOUD_CONTAINER_CLI") {
62 if std::process::Command::new(&cli)
63 .arg("info")
64 .stdout(std::process::Stdio::null())
65 .stderr(std::process::Stdio::null())
66 .status()
67 .map(|s| s.success())
68 .unwrap_or(false)
69 {
70 cli
71 } else {
72 return None;
73 }
74 } else if is_cli_available("docker") {
75 "docker".to_string()
76 } else if is_cli_available("podman") {
77 "podman".to_string()
78 } else {
79 return None;
80 };
81
82 let instance_id = format!("fakecloud-{}", std::process::id());
83
84 let (host_alias, add_host_arg) = if is_podman_binary(&cli) {
85 ("host.containers.internal".to_string(), None)
90 } else if cfg!(target_os = "linux") {
91 let ip = detect_bridge_gateway(&cli).unwrap_or_else(|| "172.17.0.1".to_string());
96 (
97 "host.docker.internal".to_string(),
98 Some(format!("host.docker.internal:{ip}")),
99 )
100 } else {
101 (
104 "host.docker.internal".to_string(),
105 Some("host.docker.internal:host-gateway".to_string()),
106 )
107 };
108
109 let docker_config = build_local_registry_docker_config(server_port).map(Arc::new);
110 let sibling_host = resolve_sibling_host(std::env::var("FAKECLOUD_IN_CONTAINER").ok());
111 Some(Self {
112 cli,
113 instance_id,
114 host_alias,
115 add_host_arg,
116 server_port,
117 sibling_host,
118 docker_config,
119 })
120 }
121
122 fn apply_host_alias(&self, cmd: &mut tokio::process::Command) {
126 if let Some(arg) = &self.add_host_arg {
127 cmd.arg("--add-host").arg(arg);
128 }
129 }
130
131 fn docker_config_path(&self) -> Option<PathBuf> {
132 self.docker_config.as_ref().map(|d| d.path().to_path_buf())
133 }
134
135 async fn start_image_container(
141 &self,
142 func: &LambdaFunction,
143 layers: &[Vec<u8>],
144 ) -> Result<WarmInstance, RuntimeError> {
145 let image = func.image_uri.as_deref().ok_or_else(|| {
146 RuntimeError::ContainerStartFailed("PackageType=Image function has no ImageUri".into())
147 })?;
148
149 let local_pull_uri = fakecloud_core::ecr_uri::translate_to_local_at(
156 image,
157 &self.sibling_host,
158 self.server_port,
159 );
160 let pull_uri = local_pull_uri.as_deref().unwrap_or(image);
161
162 let mut pull_cmd = tokio::process::Command::new(&self.cli);
163 if let Some(p) = self.docker_config_path() {
164 pull_cmd.env("DOCKER_CONFIG", p);
165 }
166 let pull_out = pull_cmd
167 .args(["pull", pull_uri])
168 .output()
169 .await
170 .map_err(|e| RuntimeError::ContainerStartFailed(format!("docker pull: {e}")))?;
171 if !pull_out.status.success() {
172 return Err(RuntimeError::ContainerStartFailed(format!(
173 "docker pull failed: {}",
174 String::from_utf8_lossy(&pull_out.stderr)
175 )));
176 }
177 let run_image = if let Some(ref local_uri) = local_pull_uri {
182 if fakecloud_core::ecr_uri::is_digest_ref(image) {
183 local_uri.clone()
184 } else {
185 let _ = tokio::process::Command::new(&self.cli)
186 .args(["tag", local_uri, image])
187 .output()
188 .await;
189 image.to_string()
190 }
191 } else {
192 image.to_string()
193 };
194
195 let mut cmd = tokio::process::Command::new(&self.cli);
196 cmd.arg("create")
197 .arg("-p")
198 .arg(":8080")
199 .arg("--label")
200 .arg(format!("fakecloud-lambda={}", func.function_name))
201 .arg("--label")
202 .arg(format!("fakecloud-instance={}", self.instance_id));
203 self.apply_host_alias(&mut cmd);
204
205 for (key, value) in rewrite_localhost_envs(&func.environment, &self.host_alias) {
206 cmd.arg("-e").arg(format!("{key}={value}"));
207 }
208 cmd.arg("-e")
209 .arg(format!("AWS_LAMBDA_FUNCTION_TIMEOUT={}", func.timeout));
210
211 let tmpfs_arg = ephemeral_storage_tmpfs_arg(func.ephemeral_storage_size);
212 cmd.arg("--tmpfs").arg(tmpfs_arg);
213
214 cmd.arg(&run_image);
215
216 let output = cmd
217 .output()
218 .await
219 .map_err(|e| RuntimeError::ContainerStartFailed(e.to_string()))?;
220 if !output.status.success() {
221 return Err(RuntimeError::ContainerStartFailed(
222 String::from_utf8_lossy(&output.stderr).to_string(),
223 ));
224 }
225 let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
226
227 if let Err(e) = self.copy_layers_into(&container_id, layers).await {
228 self.remove_container(&container_id).await;
229 return Err(e);
230 }
231
232 let start_result = tokio::process::Command::new(&self.cli)
233 .args(["start", &container_id])
234 .output()
235 .await
236 .map_err(|e| RuntimeError::ContainerStartFailed(e.to_string()))?;
237 if !start_result.status.success() {
238 self.remove_container(&container_id).await;
239 return Err(RuntimeError::ContainerStartFailed(format!(
240 "docker start failed: {}",
241 String::from_utf8_lossy(&start_result.stderr)
242 )));
243 }
244
245 let port = self.query_host_port(&container_id).await?;
246 self.wait_for_ready(&container_id, port).await?;
247
248 tracing::info!(
249 function = %func.function_name,
250 container_id = %container_id,
251 port = port,
252 image = %image,
253 "Lambda image container started"
254 );
255
256 Ok(WarmInstance {
257 endpoint: format!("{}:{port}", self.sibling_host),
258 handle: BackendHandle::Container { id: container_id },
259 })
260 }
261
262 async fn start_zip_container(
263 &self,
264 func: &LambdaFunction,
265 zip_bytes: &[u8],
266 layers: &[Vec<u8>],
267 ) -> Result<WarmInstance, RuntimeError> {
268 let image = runtime_to_image(&func.runtime)
269 .ok_or_else(|| RuntimeError::UnsupportedRuntime(func.runtime.clone()))?;
270
271 let code_dir =
274 TempDir::new().map_err(|e| RuntimeError::ZipExtractionFailed(e.to_string()))?;
275 let zip_bytes = zip_bytes.to_vec();
276 let code_path = code_dir.path().to_path_buf();
277 tokio::task::spawn_blocking(move || extract_zip(&zip_bytes, &code_path))
278 .await
279 .map_err(|e| RuntimeError::ZipExtractionFailed(e.to_string()))??;
280
281 let mut cmd = tokio::process::Command::new(&self.cli);
283 cmd.arg("create")
284 .arg("-p")
285 .arg(":8080")
286 .arg("--label")
287 .arg(format!("fakecloud-lambda={}", func.function_name))
288 .arg("--label")
289 .arg(format!("fakecloud-instance={}", self.instance_id));
290 self.apply_host_alias(&mut cmd);
291
292 for (key, value) in rewrite_localhost_envs(&func.environment, &self.host_alias) {
293 cmd.arg("-e").arg(format!("{key}={value}"));
294 }
295
296 cmd.arg("-e")
297 .arg(format!("AWS_LAMBDA_FUNCTION_TIMEOUT={}", func.timeout));
298
299 let tmpfs_arg = ephemeral_storage_tmpfs_arg(func.ephemeral_storage_size);
300 cmd.arg("--tmpfs").arg(tmpfs_arg);
301
302 cmd.arg(&image).arg(&func.handler);
303
304 let output = cmd
305 .output()
306 .await
307 .map_err(|e| RuntimeError::ContainerStartFailed(e.to_string()))?;
308
309 if !output.status.success() {
310 let stderr = String::from_utf8_lossy(&output.stderr);
311 return Err(RuntimeError::ContainerStartFailed(stderr.to_string()));
312 }
313
314 let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
315
316 let cp_result = tokio::process::Command::new(&self.cli)
318 .arg("cp")
319 .arg(format!("{}/.", code_dir.path().display()))
320 .arg(format!("{}:/var/task", container_id))
321 .output()
322 .await
323 .map_err(|e| RuntimeError::ContainerStartFailed(e.to_string()))?;
324
325 if !cp_result.status.success() {
326 self.remove_container(&container_id).await;
327 let stderr = String::from_utf8_lossy(&cp_result.stderr);
328 return Err(RuntimeError::ContainerStartFailed(format!(
329 "docker cp failed: {stderr}"
330 )));
331 }
332
333 if func.runtime.starts_with("provided") {
335 let cp_runtime = tokio::process::Command::new(&self.cli)
336 .arg("cp")
337 .arg(format!("{}/.", code_dir.path().display()))
338 .arg(format!("{}:/var/runtime", container_id))
339 .output()
340 .await
341 .map_err(|e| RuntimeError::ContainerStartFailed(e.to_string()))?;
342
343 if !cp_runtime.status.success() {
344 self.remove_container(&container_id).await;
345 let stderr = String::from_utf8_lossy(&cp_runtime.stderr);
346 return Err(RuntimeError::ContainerStartFailed(format!(
347 "docker cp to /var/runtime failed: {stderr}"
348 )));
349 }
350 }
351
352 if let Err(e) = self.copy_layers_into(&container_id, layers).await {
353 self.remove_container(&container_id).await;
354 return Err(e);
355 }
356
357 let start_result = tokio::process::Command::new(&self.cli)
360 .args(["start", &container_id])
361 .output()
362 .await
363 .map_err(|e| RuntimeError::ContainerStartFailed(e.to_string()))?;
364
365 if !start_result.status.success() {
366 self.remove_container(&container_id).await;
367 let stderr = String::from_utf8_lossy(&start_result.stderr);
368 return Err(RuntimeError::ContainerStartFailed(format!(
369 "docker start failed: {stderr}"
370 )));
371 }
372
373 let port = self.query_host_port(&container_id).await?;
374 self.wait_for_ready(&container_id, port).await?;
375
376 tracing::info!(
377 function = %func.function_name,
378 container_id = %container_id,
379 port = port,
380 runtime = %func.runtime,
381 "Lambda container started"
382 );
383
384 Ok(WarmInstance {
385 endpoint: format!("{}:{port}", self.sibling_host),
386 handle: BackendHandle::Container { id: container_id },
387 })
388 }
389
390 async fn query_host_port(&self, container_id: &str) -> Result<u16, RuntimeError> {
391 let port_output = tokio::process::Command::new(&self.cli)
392 .args(["port", container_id, "8080"])
393 .output()
394 .await
395 .map_err(|e| RuntimeError::ContainerStartFailed(e.to_string()))?;
396 let port_str = String::from_utf8_lossy(&port_output.stdout);
397 port_str
398 .trim()
399 .rsplit(':')
400 .next()
401 .and_then(|p| p.parse().ok())
402 .ok_or_else(|| {
403 RuntimeError::ContainerStartFailed(format!(
404 "could not determine port from: {}",
405 port_str.trim()
406 ))
407 })
408 }
409
410 async fn wait_for_ready(&self, container_id: &str, port: u16) -> Result<(), RuntimeError> {
411 for _ in 0..20 {
412 tokio::time::sleep(Duration::from_millis(500)).await;
413 if tokio::net::TcpStream::connect(format!("{}:{port}", self.sibling_host))
414 .await
415 .is_ok()
416 {
417 return Ok(());
418 }
419 }
420 self.remove_container(container_id).await;
421 Err(RuntimeError::ContainerStartFailed(
422 "container did not become ready within 10 seconds".to_string(),
423 ))
424 }
425
426 async fn copy_layers_into(
433 &self,
434 container_id: &str,
435 layers: &[Vec<u8>],
436 ) -> Result<(), RuntimeError> {
437 if layers.is_empty() {
438 return Ok(());
439 }
440 let layers_dir =
441 TempDir::new().map_err(|e| RuntimeError::ZipExtractionFailed(e.to_string()))?;
442 let layers_path = layers_dir.path().to_path_buf();
443 let layers_owned: Vec<Vec<u8>> = layers.to_vec();
444 tokio::task::spawn_blocking(move || {
445 for bytes in &layers_owned {
446 extract_zip(bytes, &layers_path)?;
447 }
448 Ok::<_, RuntimeError>(())
449 })
450 .await
451 .map_err(|e| RuntimeError::ZipExtractionFailed(e.to_string()))??;
452
453 let cp_result = tokio::process::Command::new(&self.cli)
454 .arg("cp")
455 .arg(format!("{}/.", layers_dir.path().display()))
456 .arg(format!("{}:/opt", container_id))
457 .output()
458 .await
459 .map_err(|e| RuntimeError::ContainerStartFailed(e.to_string()))?;
460 if !cp_result.status.success() {
461 let stderr = String::from_utf8_lossy(&cp_result.stderr);
462 return Err(RuntimeError::ContainerStartFailed(format!(
463 "docker cp layers to /opt failed: {stderr}"
464 )));
465 }
466 Ok(())
467 }
468
469 async fn remove_container(&self, container_id: &str) {
471 let _ = tokio::process::Command::new(&self.cli)
472 .args(["rm", "-f", container_id])
473 .output()
474 .await;
475 }
476}
477
478#[async_trait]
479impl LambdaBackend for DockerBackend {
480 fn name(&self) -> &str {
481 &self.cli
482 }
483
484 async fn launch(
485 &self,
486 func: &LambdaFunction,
487 code_zip: Option<&[u8]>,
488 layers: &[Vec<u8>],
489 _deploy_id: &str,
490 ) -> Result<WarmInstance, RuntimeError> {
491 if func.package_type == "Image" {
492 self.start_image_container(func, layers).await
493 } else {
494 let bytes =
495 code_zip.ok_or_else(|| RuntimeError::NoCodeZip(func.function_name.clone()))?;
496 self.start_zip_container(func, bytes, layers).await
497 }
498 }
499
500 async fn terminate(&self, handle: &BackendHandle) {
501 match handle {
502 BackendHandle::Container { id } => self.remove_container(id).await,
503 BackendHandle::Pod { .. } => {}
506 }
507 }
508
509 async fn prepull_image(&self, image: &str) -> Result<(), RuntimeError> {
510 let local_uri = fakecloud_core::ecr_uri::translate_to_local_at(
514 image,
515 &self.sibling_host,
516 self.server_port,
517 );
518 let pull_uri = local_uri.as_deref().unwrap_or(image);
519
520 let mut cmd = tokio::process::Command::new(&self.cli);
521 if let Some(p) = self.docker_config_path() {
522 cmd.env("DOCKER_CONFIG", p);
523 }
524 let out = cmd
525 .args(["pull", pull_uri])
526 .output()
527 .await
528 .map_err(|e| RuntimeError::ContainerStartFailed(format!("docker pull: {e}")))?;
529 if !out.status.success() {
530 return Err(RuntimeError::ContainerStartFailed(format!(
531 "docker pull failed for {pull_uri}: {}",
532 String::from_utf8_lossy(&out.stderr)
533 )));
534 }
535 Ok(())
536 }
537}
538
539pub fn runtime_to_image(runtime: &str) -> Option<String> {
541 let (base, tag) = match runtime {
542 "python3.14" => ("python", "3.14"),
543 "python3.13" => ("python", "3.13"),
544 "python3.12" => ("python", "3.12"),
545 "python3.11" => ("python", "3.11"),
546 "python3.10" => ("python", "3.10"),
547 "python3.9" => ("python", "3.9"),
548 "python3.8" => ("python", "3.8"),
549 "nodejs24.x" => ("nodejs", "24"),
550 "nodejs22.x" => ("nodejs", "22"),
551 "nodejs20.x" => ("nodejs", "20"),
552 "nodejs18.x" => ("nodejs", "18"),
553 "nodejs16.x" => ("nodejs", "16"),
554 "ruby3.4" => ("ruby", "3.4"),
555 "ruby3.3" => ("ruby", "3.3"),
556 "java25" => ("java", "25"),
557 "java21" => ("java", "21"),
558 "java17" => ("java", "17"),
559 "java11" => ("java", "11"),
560 "dotnet10" => ("dotnet", "10"),
561 "dotnet8" => ("dotnet", "8"),
562 "go1.x" => ("go", "1"),
563 "provided.al2023" => ("provided", "al2023"),
564 "provided.al2" => ("provided", "al2"),
565 _ => return None,
566 };
567 Some(format!("public.ecr.aws/lambda/{base}:{tag}"))
568}
569
570pub(crate) fn ephemeral_storage_tmpfs_arg(size: Option<i64>) -> String {
582 let mib = size.unwrap_or(512).max(64);
583 format!("/tmp:size={mib}m,exec")
584}
585
586pub fn extract_zip(zip_bytes: &[u8], dest: &Path) -> Result<(), RuntimeError> {
588 let cursor = std::io::Cursor::new(zip_bytes);
589 let mut archive = zip::ZipArchive::new(cursor)
590 .map_err(|e| RuntimeError::ZipExtractionFailed(e.to_string()))?;
591
592 for i in 0..archive.len() {
593 let mut file = archive
594 .by_index(i)
595 .map_err(|e| RuntimeError::ZipExtractionFailed(e.to_string()))?;
596
597 let out_path = dest.join(file.enclosed_name().ok_or_else(|| {
598 RuntimeError::ZipExtractionFailed("invalid file name in ZIP".to_string())
599 })?);
600
601 if file.is_dir() {
602 std::fs::create_dir_all(&out_path)
603 .map_err(|e| RuntimeError::ZipExtractionFailed(e.to_string()))?;
604 } else {
605 if let Some(parent) = out_path.parent() {
606 std::fs::create_dir_all(parent)
607 .map_err(|e| RuntimeError::ZipExtractionFailed(e.to_string()))?;
608 }
609 let mut out_file = std::fs::File::create(&out_path)
610 .map_err(|e| RuntimeError::ZipExtractionFailed(e.to_string()))?;
611 std::io::copy(&mut file, &mut out_file)
612 .map_err(|e| RuntimeError::ZipExtractionFailed(e.to_string()))?;
613
614 #[cfg(unix)]
615 {
616 use std::os::unix::fs::PermissionsExt;
617 if let Some(mode) = file.unix_mode() {
618 std::fs::set_permissions(&out_path, std::fs::Permissions::from_mode(mode))
619 .map_err(|e| RuntimeError::ZipExtractionFailed(e.to_string()))?;
620 }
621 }
622 }
623 }
624 Ok(())
625}
626
627fn detect_bridge_gateway(cli: &str) -> Option<String> {
629 let output = std::process::Command::new(cli)
630 .args([
631 "network",
632 "inspect",
633 "bridge",
634 "--format",
635 "{{range .IPAM.Config}}{{.Gateway}}{{end}}",
636 ])
637 .output()
638 .ok()?;
639
640 if output.status.success() {
641 let gateway = String::from_utf8_lossy(&output.stdout).trim().to_string();
642 if !gateway.is_empty() && gateway.contains('.') {
643 tracing::info!(
644 gateway = %gateway,
645 "Detected Docker bridge gateway for Lambda containers"
646 );
647 return Some(gateway);
648 }
649 }
650 None
651}
652
653fn resolve_sibling_host(env_value: Option<String>) -> String {
663 let in_container = env_value
664 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
665 .unwrap_or(false);
666 if in_container {
667 "host.docker.internal".to_string()
668 } else {
669 "127.0.0.1".to_string()
670 }
671}
672
673fn is_cli_available(name: &str) -> bool {
674 std::process::Command::new(name)
675 .arg("info")
676 .stdout(std::process::Stdio::null())
677 .stderr(std::process::Stdio::null())
678 .status()
679 .map(|s| s.success())
680 .unwrap_or(false)
681}
682
683fn is_podman_binary(cli: &str) -> bool {
688 std::path::Path::new(cli)
689 .file_name()
690 .and_then(|n| n.to_str())
691 .map(|n| n.contains("podman"))
692 .unwrap_or(false)
693}
694
695fn build_local_registry_docker_config(server_port: u16) -> Option<TempDir> {
696 let dir = TempDir::new().ok()?;
697 let auth = base64::engine::general_purpose::STANDARD.encode("AWS:fakecloud-lambda-runtime");
698 let config = serde_json::json!({
703 "auths": {
704 format!("127.0.0.1:{server_port}"): { "auth": auth },
705 format!("host.docker.internal:{server_port}"): { "auth": auth },
706 }
707 });
708 std::fs::write(dir.path().join("config.json"), config.to_string()).ok()?;
709 Some(dir)
710}
711
712#[cfg(test)]
713mod tests {
714 use std::io::{Read, Write};
715
716 use super::*;
717
718 #[test]
719 fn test_runtime_to_image() {
720 assert_eq!(
721 runtime_to_image("python3.12"),
722 Some("public.ecr.aws/lambda/python:3.12".to_string())
723 );
724 assert_eq!(
725 runtime_to_image("nodejs20.x"),
726 Some("public.ecr.aws/lambda/nodejs:20".to_string())
727 );
728 assert_eq!(
729 runtime_to_image("provided.al2023"),
730 Some("public.ecr.aws/lambda/provided:al2023".to_string())
731 );
732 assert_eq!(
733 runtime_to_image("ruby3.4"),
734 Some("public.ecr.aws/lambda/ruby:3.4".to_string())
735 );
736 assert_eq!(
737 runtime_to_image("java21"),
738 Some("public.ecr.aws/lambda/java:21".to_string())
739 );
740 assert_eq!(
741 runtime_to_image("dotnet8"),
742 Some("public.ecr.aws/lambda/dotnet:8".to_string())
743 );
744 assert_eq!(
745 runtime_to_image("nodejs16.x"),
746 Some("public.ecr.aws/lambda/nodejs:16".to_string())
747 );
748 assert_eq!(
749 runtime_to_image("python3.10"),
750 Some("public.ecr.aws/lambda/python:3.10".to_string())
751 );
752 assert_eq!(
753 runtime_to_image("python3.9"),
754 Some("public.ecr.aws/lambda/python:3.9".to_string())
755 );
756 assert_eq!(
757 runtime_to_image("python3.8"),
758 Some("public.ecr.aws/lambda/python:3.8".to_string())
759 );
760 assert_eq!(
761 runtime_to_image("java11"),
762 Some("public.ecr.aws/lambda/java:11".to_string())
763 );
764 assert_eq!(
765 runtime_to_image("go1.x"),
766 Some("public.ecr.aws/lambda/go:1".to_string())
767 );
768 assert_eq!(
769 runtime_to_image("nodejs24.x"),
770 Some("public.ecr.aws/lambda/nodejs:24".to_string())
771 );
772 assert_eq!(
773 runtime_to_image("python3.14"),
774 Some("public.ecr.aws/lambda/python:3.14".to_string())
775 );
776 assert_eq!(
777 runtime_to_image("java25"),
778 Some("public.ecr.aws/lambda/java:25".to_string())
779 );
780 assert_eq!(
781 runtime_to_image("dotnet10"),
782 Some("public.ecr.aws/lambda/dotnet:10".to_string())
783 );
784 assert_eq!(runtime_to_image("unknown"), None);
785 }
786
787 #[test]
788 fn is_podman_binary_matches_bare_name() {
789 assert!(is_podman_binary("podman"));
790 assert!(is_podman_binary("podman-remote"));
791 }
792
793 #[test]
794 fn is_podman_binary_matches_absolute_path() {
795 assert!(is_podman_binary("/opt/homebrew/bin/podman"));
796 assert!(is_podman_binary("/usr/local/bin/podman-remote"));
797 }
798
799 #[test]
800 fn is_podman_binary_rejects_docker() {
801 assert!(!is_podman_binary("docker"));
802 assert!(!is_podman_binary("/usr/local/bin/docker"));
803 assert!(!is_podman_binary("docker-credential-helper"));
805 }
806
807 #[test]
808 fn resolve_sibling_host_defaults_to_loopback() {
809 assert_eq!(resolve_sibling_host(None), "127.0.0.1");
810 assert_eq!(resolve_sibling_host(Some("".to_string())), "127.0.0.1");
811 assert_eq!(resolve_sibling_host(Some("0".to_string())), "127.0.0.1");
812 assert_eq!(resolve_sibling_host(Some("false".to_string())), "127.0.0.1");
813 }
814
815 #[test]
816 fn resolve_sibling_host_uses_docker_internal_when_in_container() {
817 assert_eq!(
818 resolve_sibling_host(Some("1".to_string())),
819 "host.docker.internal"
820 );
821 assert_eq!(
822 resolve_sibling_host(Some("true".to_string())),
823 "host.docker.internal"
824 );
825 assert_eq!(
826 resolve_sibling_host(Some("TRUE".to_string())),
827 "host.docker.internal"
828 );
829 }
830
831 #[test]
832 fn test_extract_zip() {
833 let buf = Vec::new();
834 let cursor = std::io::Cursor::new(buf);
835 let mut writer = zip::ZipWriter::new(cursor);
836 let options = zip::write::SimpleFileOptions::default();
837 writer.start_file("handler.py", options).unwrap();
838 writer
839 .write_all(b"def handler(event, context):\n return {'statusCode': 200}\n")
840 .unwrap();
841 let cursor = writer.finish().unwrap();
842 let zip_bytes = cursor.into_inner();
843
844 let dir = TempDir::new().unwrap();
845 extract_zip(&zip_bytes, dir.path()).unwrap();
846
847 let handler_path = dir.path().join("handler.py");
848 assert!(handler_path.exists());
849
850 let mut content = String::new();
851 std::fs::File::open(&handler_path)
852 .unwrap()
853 .read_to_string(&mut content)
854 .unwrap();
855 assert!(content.contains("def handler"));
856 }
857
858 #[test]
859 fn ephemeral_storage_tmpfs_arg_defaults_to_512_when_none() {
860 assert_eq!(ephemeral_storage_tmpfs_arg(None), "/tmp:size=512m,exec");
864 }
865
866 #[test]
867 fn ephemeral_storage_tmpfs_arg_uses_supplied_size() {
868 assert_eq!(
869 ephemeral_storage_tmpfs_arg(Some(2048)),
870 "/tmp:size=2048m,exec"
871 );
872 assert_eq!(
873 ephemeral_storage_tmpfs_arg(Some(10240)),
874 "/tmp:size=10240m,exec"
875 );
876 }
877
878 #[test]
879 fn ephemeral_storage_tmpfs_arg_clamps_to_64_floor() {
880 assert_eq!(ephemeral_storage_tmpfs_arg(Some(0)), "/tmp:size=64m,exec");
884 assert_eq!(ephemeral_storage_tmpfs_arg(Some(32)), "/tmp:size=64m,exec");
885 }
886}