1#![allow(clippy::let_and_return, clippy::let_unit_value)]
5#![warn(clippy::dbg_macro, clippy::unwrap_used)]
6
7mod args;
8
9use std::env;
10use std::env::temp_dir;
11use std::ffi::OsStr;
12use std::ffi::OsString;
13use std::fs;
14use std::future::ready;
15use std::future::Future;
16use std::io;
17use std::ops::Deref;
18use std::ops::DerefMut;
19use std::path::Path;
20use std::process;
21use std::process::ExitStatus;
22use std::process::Stdio;
23use std::ptr;
24use std::str::FromStr as _;
25use std::time::Duration;
26
27use anyhow::Context as _;
28use anyhow::Result;
29
30use clap::error::ErrorKind;
31use clap::Parser as _;
32
33use futures_util::join;
34use futures_util::TryFutureExt as _;
35
36use fs4::tokio::AsyncFileExt as _;
37
38use tar::Archive;
39
40use tokio::fs::canonicalize;
41use tokio::fs::copy;
42use tokio::fs::create_dir_all;
43use tokio::fs::remove_dir_all;
44use tokio::fs::remove_file;
45use tokio::fs::try_exists;
46use tokio::fs::File;
47use tokio::io::AsyncReadExt as _;
48use tokio::io::AsyncSeekExt as _;
49use tokio::io::AsyncWriteExt as _;
50use tokio::process::Command as Process;
51use tokio::task::spawn_blocking;
52use tokio::time::sleep;
53
54use xz2::read::XzDecoder;
55
56use crate::args::Args;
57use crate::args::Command;
58use crate::args::Deploy;
59
60
61fn file_stem(path: &Path) -> Result<&OsStr> {
66 let mut last_stem = path.as_os_str();
67
68 loop {
69 let stem = Path::new(last_stem)
70 .file_stem()
71 .with_context(|| format!("failed to extract file stem of path `{}`", path.display()))?;
72
73 if stem == last_stem {
74 break Ok(stem)
75 }
76
77 last_stem = stem;
78 }
79}
80
81
82#[derive(Debug)]
83struct FileLockGuard<'file> {
84 file: Option<&'file mut File>,
86}
87
88impl<'file> FileLockGuard<'file> {
89 async fn lock(file: &'file mut File) -> Result<Self> {
90 loop {
91 let locked = file.try_lock_exclusive().context("failed to lock file")?;
95 if locked {
96 let slf = Self { file: Some(file) };
97 break Ok(slf)
98 } else {
99 let () = sleep(Duration::from_millis(100)).await;
100 }
101 }
102 }
103
104 #[cfg(test)]
105 fn unlock(mut self) -> Result<&'file mut File> {
106 let file = self.file.take().expect("lock guard without a locked file");
107 let () = file.unlock().context("failed to unlock file")?;
108 Ok(file)
109 }
110}
111
112impl Deref for FileLockGuard<'_> {
113 type Target = File;
114
115 fn deref(&self) -> &Self::Target {
116 self
117 .file
118 .as_ref()
119 .expect("lock guard without a locked file")
120 }
121}
122
123impl DerefMut for FileLockGuard<'_> {
124 fn deref_mut(&mut self) -> &mut Self::Target {
125 self
126 .file
127 .as_mut()
128 .expect("lock guard without a locked file")
129 }
130}
131
132impl Drop for FileLockGuard<'_> {
133 fn drop(&mut self) {
134 if let Some(file) = self.file.as_ref() {
135 let () = file.unlock().expect("failed to unlock file");
137 }
138 }
139}
140
141
142async fn read_pids(file: &mut FileLockGuard<'_>) -> Result<Vec<u32>> {
143 let _offset = file.rewind().await?;
144 let mut data = String::new();
145 let _count = file.read_to_string(&mut data).await?;
146
147 if data.is_empty() {
148 Ok(Vec::new())
149 } else {
150 let mut pids = data
151 .lines()
152 .filter(|line| !line.is_empty())
153 .filter_map(|line| {
154 u32::from_str(line).ok()
157 })
158 .collect::<Vec<u32>>();
159
160 let () = pids.sort_unstable();
161 Ok(pids)
162 }
163}
164
165
166async fn write_pids(file: &mut FileLockGuard<'_>, pids: &[u32]) -> Result<()> {
167 let _offset = file.rewind().await?;
168 let () = file
169 .set_len(0)
170 .await
171 .context("failed to truncate PID file")?;
172
173 let content = pids
174 .iter()
175 .map(|pid| pid.to_string())
176 .collect::<Vec<_>>()
177 .join("\n");
178 let () = file
179 .write_all(content.as_bytes())
180 .await
181 .context("failed to write PIDs")?;
182 Ok(())
183}
184
185
186#[derive(Debug)]
188enum AddPidResult<'file> {
189 #[allow(dead_code)]
191 OwnPid(FileLockGuard<'file>),
192 ExistingPid(u32),
195}
196
197async fn add_pid(ref_file: &mut File, pid: u32) -> Result<AddPidResult<'_>> {
198 let mut guard = FileLockGuard::lock(ref_file).await?;
199 let mut pids = read_pids(&mut guard).await?;
200 let first_pid = pids.first().copied();
201
202 if let Err(idx) = pids.binary_search(&pid) {
203 let () = pids.insert(idx, pid);
204 }
205
206 let () = write_pids(&mut guard, &pids).await?;
207 if let Some(pid) = first_pid {
208 Ok(AddPidResult::ExistingPid(pid))
209 } else {
210 Ok(AddPidResult::OwnPid(guard))
211 }
212}
213
214async fn remove_pid(ref_file: &mut File, pid: u32) -> Result<Option<FileLockGuard<'_>>> {
218 let mut guard = FileLockGuard::lock(ref_file).await?;
219 let mut pids = read_pids(&mut guard).await?;
220
221 if let Ok(idx) = pids.binary_search(&pid) {
222 let _pid = pids.remove(idx);
223 } else {
224 }
229
230 let () = write_pids(&mut guard, &pids).await?;
231 let guard = if pids.is_empty() { Some(guard) } else { None };
232 Ok(guard)
233}
234
235
236async fn with_ref<S, FutS, B, FutB, C, FutC>(
237 ref_path: &Path,
238 setup: S,
239 body: B,
240 cleanup: C,
241) -> Result<()>
242where
243 S: FnOnce() -> FutS,
244 FutS: Future<Output = Result<()>>,
245 B: FnOnce(Option<u32>) -> FutB,
246 FutB: Future<Output = Result<()>>,
247 C: FnOnce(bool) -> FutC,
248 FutC: Future<Output = Result<()>>,
249{
250 let mut ref_file = File::options()
251 .create(true)
252 .read(true)
253 .write(true)
254 .truncate(false)
255 .open(ref_path)
256 .await
257 .with_context(|| format!("failed to open `{}`", ref_path.display()))?;
258
259 let pid = process::id();
260 let add_result = add_pid(&mut ref_file, pid).await?;
261 let (is_first_user, ns_pid) = match &add_result {
262 AddPidResult::OwnPid(_) => (true, None),
263 AddPidResult::ExistingPid(pid) => (false, Some(*pid)),
264 };
265
266 if is_first_user {
267 let setup_result = setup().await;
268 let () = drop(add_result);
269
270 if let Err(setup_err) = setup_result {
273 match remove_pid(&mut ref_file, pid).await {
274 Ok(Some(_guard)) => {
275 let _result = remove_file(ref_path).await;
277 return Err(setup_err)
278 },
279 Ok(None) => return Err(setup_err),
280 Err(inner_err) => return Err(setup_err.context(inner_err)),
281 }
282 }
283 } else {
284 let () = drop(add_result);
285 }
286
287
288 let body_result = body(ns_pid).await;
289 let result = match remove_pid(&mut ref_file, pid).await {
290 Ok(Some(guard)) => {
291 let cleanup_result = cleanup(is_first_user).await;
292 let _result = remove_file(ref_path).await;
294 let () = drop(guard);
295 body_result.and(cleanup_result)
296 },
297 Ok(None) => body_result,
298 Err(inner_err) => {
299 body_result.map_err(|err| err.context(inner_err))
304 },
305 };
306
307 let () = drop(ref_file);
308 result
309}
310
311
312async fn unpack_compressed_tar(archive: &Path, dst: &Path) -> Result<()> {
314 let () = create_dir_all(dst)
315 .await
316 .with_context(|| format!("failed to create directory `{}`", dst.display()))?;
317 let archive = archive.to_path_buf();
318 let dst = dst.to_path_buf();
319
320 let result = spawn_blocking(move || {
322 let file = fs::File::open(&archive).context("failed to open archive")?;
323 let decoder = XzDecoder::new_multi_decoder(file);
324 let mut extracter = Archive::new(decoder);
325 let () = extracter.set_overwrite(true);
326 let () = extracter.set_preserve_ownerships(true);
327 let () = extracter.set_preserve_permissions(true);
328 let () = extracter.unpack(dst).context("failed to unpack archive")?;
329 Ok(())
330 })
331 .await?;
332
333 result
334}
335
336
337fn concat_command<C, A, S>(command: C, args: A) -> OsString
339where
340 C: AsRef<OsStr>,
341 A: IntoIterator<Item = S>,
342 S: AsRef<OsStr>,
343{
344 args
345 .into_iter()
346 .fold(command.as_ref().to_os_string(), |mut cmd, arg| {
347 cmd.push(OsStr::new(" "));
348 cmd.push(arg.as_ref());
349 cmd
350 })
351}
352
353
354fn format_command<C, A, S>(command: C, args: A) -> String
356where
357 C: AsRef<OsStr>,
358 A: IntoIterator<Item = S>,
359 S: AsRef<OsStr>,
360{
361 concat_command(command, args).to_string_lossy().to_string()
362}
363
364fn evaluate<C, A, S>(
365 status: ExitStatus,
366 command: C,
367 args: A,
368 stderr: Option<&[u8]>,
369) -> io::Result<()>
370where
371 C: AsRef<OsStr>,
372 A: IntoIterator<Item = S>,
373 S: AsRef<OsStr>,
374{
375 if !status.success() {
376 let code = if let Some(code) = status.code() {
377 format!(" ({code})")
378 } else {
379 " (terminated by signal)".to_string()
380 };
381
382 let stderr = String::from_utf8_lossy(stderr.unwrap_or(&[]));
383 let stderr = stderr.trim_end();
384 let stderr = if !stderr.is_empty() {
385 format!(": {stderr}")
386 } else {
387 String::new()
388 };
389
390 Err(io::Error::other(format!(
391 "`{}` reported non-zero exit-status{code}{stderr}",
392 format_command(command, args)
393 )))
394 } else {
395 Ok(())
396 }
397}
398
399async fn run_command<C, A, S>(command: C, args: A) -> io::Result<()>
401where
402 C: AsRef<OsStr>,
403 A: IntoIterator<Item = S> + Clone,
404 S: AsRef<OsStr>,
405{
406 let output = Process::new(command.as_ref())
407 .stdin(Stdio::inherit())
408 .stdout(Stdio::inherit())
409 .stderr(Stdio::inherit())
410 .env_clear()
411 .envs(env::vars().filter(|(k, _)| k == "PATH"))
412 .args(args.clone())
413 .output()
414 .await
415 .map_err(|err| {
416 io::Error::other(format!(
417 "failed to run `{}`: {err}",
418 format_command(command.as_ref(), args.clone())
419 ))
420 })?;
421
422 let () = evaluate(output.status, command, args, Some(&output.stderr))?;
423 Ok(())
424}
425
426async fn check_command<C, A, S>(command: C, args: A) -> io::Result<()>
428where
429 C: AsRef<OsStr>,
430 A: IntoIterator<Item = S> + Clone,
431 S: AsRef<OsStr>,
432{
433 let status = Process::new(command.as_ref())
434 .stdin(Stdio::inherit())
435 .stdout(Stdio::inherit())
436 .stderr(Stdio::inherit())
437 .env_clear()
438 .envs(env::vars().filter(|(k, _)| k == "PATH"))
439 .args(args.clone())
440 .status()
441 .await
442 .map_err(|err| {
443 io::Error::other(format!(
444 "failed to run `{}`: {err}",
445 format_command(command.as_ref(), args.clone())
446 ))
447 })?;
448
449 let () = evaluate(status, command, args, None)?;
450 Ok(())
451}
452
453
454fn create_namespace() -> Result<()> {
460 let rc = unsafe { libc::unshare(libc::CLONE_NEWNS) };
462 if rc != 0 {
463 return Err(io::Error::last_os_error()).context("failed to create mount namespace");
464 }
465
466 let rc = unsafe {
468 libc::mount(
469 ptr::null(),
470 c"/".as_ptr(),
471 ptr::null(),
472 libc::MS_REC | libc::MS_PRIVATE,
473 ptr::null(),
474 )
475 };
476 if rc != 0 {
477 return Err(io::Error::last_os_error()).context("failed to set mount propagation to private");
478 }
479
480 Ok(())
481}
482
483
484async fn setup_chroot(archive: &Path, chroot: &Path) -> Result<()> {
485 let () = create_namespace()?;
487
488 let present = try_exists(chroot)
489 .await
490 .with_context(|| format!("failed to check existence of `{}`", chroot.display()))?;
491
492 if !present {
493 let () = unpack_compressed_tar(archive, chroot)
494 .await
495 .with_context(|| {
496 format!(
497 "failed to extract archive `{}` into chroot `{}`",
498 archive.display(),
499 chroot.display()
500 )
501 })?;
502 }
503
504 let proc = chroot.join("proc");
505 let proc = run_command(
506 "mount",
507 [
508 OsStr::new("-t"),
509 OsStr::new("proc"),
510 OsStr::new("proc"),
511 proc.as_os_str(),
512 ],
513 );
514 let dev = chroot.join("dev");
515 let dev = create_dir_all(&dev).and_then(|()| {
516 run_command(
517 "mount",
518 [OsStr::new("--rbind"), OsStr::new("/dev"), dev.as_os_str()],
519 )
520 });
521 let sys = chroot.join("sys");
522 let sys = create_dir_all(&sys).and_then(|()| {
523 run_command(
524 "mount",
525 [OsStr::new("--rbind"), OsStr::new("/sys"), sys.as_os_str()],
526 )
527 });
528 let repos = chroot.join("var").join("db").join("repos");
529 let repos = create_dir_all(&repos).and_then(|()| {
530 run_command(
531 "mount",
532 [
533 OsStr::new("--bind"),
534 OsStr::new("/var/db/repos"),
535 repos.as_os_str(),
536 ],
537 )
538 });
539 let tmp = chroot.join("tmp");
540 let tmp = create_dir_all(&tmp).and_then(|()| {
541 run_command(
542 "mount",
543 [OsStr::new("--bind"), OsStr::new("/tmp"), tmp.as_os_str()],
544 )
545 });
546 let run = chroot.join("run");
547 let run = create_dir_all(&run).and_then(|()| {
548 run_command(
549 "mount",
550 [OsStr::new("--bind"), OsStr::new("/run"), run.as_os_str()],
551 )
552 });
553 let resolve = canonicalize(Path::new("/etc/resolv.conf"))
554 .and_then(|resolve| copy(resolve, chroot.join("etc").join("resolv.conf")))
555 .and_then(|_count| ready(Ok(())));
556 let results = <[_; 7]>::from(join!(proc, dev, sys, repos, tmp, run, resolve));
557 let () = results.into_iter().try_for_each(|result| result)?;
558 Ok(())
559}
560
561async fn chroot(
562 chroot_dir: &Path,
563 ns_pid: Option<u32>,
564 command: Option<&[OsString]>,
565 user: Option<&OsStr>,
566) -> Result<()> {
567 let su_args = [
568 OsStr::new("/bin/su"),
569 OsStr::new("--login"),
570 user.unwrap_or_else(|| OsStr::new("root")),
571 ];
572 let su_args = su_args.as_slice();
573
574 let session_command = if let Some([cmd, args @ ..]) = command {
575 format_command(cmd, args)
576 } else {
577 let args = [
578 r#"PS1="(chroot) \[\033[01;32m\]\u@\h\[\033[01;34m\] \w \$\[\033[00m\] ""#,
579 "bash",
580 "--norc",
581 "-i",
582 ];
583 format_command("/bin/env", args)
584 };
585
586 if let Some(pid) = ns_pid {
587 let target_arg = format!("--target={pid}");
589
590 let () = check_command(
591 "nsenter",
592 [
593 OsStr::new(&target_arg),
594 OsStr::new("--mount"),
595 OsStr::new("--"),
596 ]
597 .into_iter()
598 .chain(
599 [OsStr::new("chroot"), chroot_dir.as_os_str()]
600 .into_iter()
601 .chain(su_args.iter().copied())
602 .chain([
603 OsStr::new("--session-command"),
604 OsStr::new(&session_command),
605 ]),
606 ),
607 )
608 .await?;
609 } else {
610 let () = check_command(
612 "chroot",
613 [chroot_dir.as_os_str()]
614 .as_slice()
615 .iter()
616 .chain(su_args)
617 .chain(
618 [
619 OsStr::new("--session-command"),
620 OsStr::new(&session_command),
621 ]
622 .as_slice(),
623 ),
624 )
625 .await?;
626 }
627 Ok(())
628}
629
630async fn cleanup_chroot(chroot: &Path, is_first_user: bool, remove: bool) -> Result<()> {
631 if is_first_user {
637 let run = run_command("umount", [chroot.join("run")]);
638 let tmp = run_command("umount", [chroot.join("tmp")]);
639 let repos = run_command("umount", [chroot.join("var").join("db").join("repos")]);
640 let proc = run_command("umount", [chroot.join("proc")]);
641 let sys = run_command(
642 "umount",
643 [
644 OsString::from("--recursive"),
645 chroot.join("sys").into_os_string(),
646 ],
647 );
648 let dev = run_command(
649 "umount",
650 [
651 OsString::from("--recursive"),
652 chroot.join("dev").into_os_string(),
653 ],
654 );
655
656 let results = join!(dev, sys, repos, tmp, run);
657 let result = proc.await;
661 let () = <[_; 5]>::from(results)
662 .into_iter()
663 .chain([result])
664 .try_for_each(|result| result)?;
665 }
666
667 if remove {
668 let () = remove_dir_all(chroot).await?;
669 }
670 Ok(())
671}
672
673async fn deploy(deploy: Deploy) -> Result<()> {
675 let Deploy {
676 archive,
677 command,
678 user,
679 remove,
680 } = deploy;
681
682 let tmp = temp_dir();
683 let stem = file_stem(&archive).with_context(|| {
684 format!(
685 "failed to extract file stem of path `{}`",
686 archive.display()
687 )
688 })?;
689
690 let chroot_dir = tmp.join(stem);
691 let ref_path = tmp.join(stem).with_extension("lck");
692
693 let setup = || setup_chroot(&archive, &chroot_dir);
694 let chroot = |ns_pid| chroot(&chroot_dir, ns_pid, command.as_deref(), user.as_deref());
695 let cleanup = |is_first_user| cleanup_chroot(&chroot_dir, is_first_user, remove);
696
697 with_ref(&ref_path, setup, chroot, cleanup).await
698}
699
700
701pub async fn run<A, T>(args: A) -> Result<()>
703where
704 A: IntoIterator<Item = T>,
705 T: Into<OsString> + Clone,
706{
707 let args = match Args::try_parse_from(args) {
708 Ok(args) => args,
709 Err(err) => match err.kind() {
710 ErrorKind::DisplayVersion
711 | ErrorKind::DisplayHelp
712 | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => {
713 print!("{}", err);
714 return Ok(())
715 },
716 _ => return Err(err.into()),
717 },
718 };
719
720 match args.command {
721 Command::Deploy(deploy) => self::deploy(deploy).await,
722 }
723}
724
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729
730 use std::ffi::OsStr;
731
732 use anyhow::anyhow;
733
734 use tempfile::tempfile;
735 use tempfile::NamedTempFile;
736
737 use tokio::select;
738
739
740 #[test]
742 fn file_stem_extraction() {
743 let path = Path::new("/tmp/stage3-amd64-openrc-20240211T161834Z.tar.xz");
744 let stem = file_stem(path).unwrap();
745 assert_eq!(stem, OsStr::new("stage3-amd64-openrc-20240211T161834Z"));
746 }
747
748 #[tokio::test]
750 async fn lock_file_add_pid() {
751 let mut file = File::from_std(tempfile().unwrap());
752
753 let AddPidResult::OwnPid(mut guard) = add_pid(&mut file, 1234).await.unwrap() else {
754 panic!("expected OwnPid");
755 };
756 let pids = read_pids(&mut guard).await.unwrap();
757 assert_eq!(pids, vec![1234]);
758 }
759
760 #[tokio::test]
762 async fn lock_file_add_pid_multi() {
763 let mut file = File::from_std(tempfile().unwrap());
764
765 let result = add_pid(&mut file, 1000).await.unwrap();
766 assert!(matches!(result, AddPidResult::OwnPid(_)));
767 drop(result);
768
769 let result = add_pid(&mut file, 2000).await.unwrap();
770 assert!(matches!(result, AddPidResult::ExistingPid(1000)));
771 drop(result);
772
773 let result = add_pid(&mut file, 3000).await.unwrap();
774 assert!(matches!(result, AddPidResult::ExistingPid(1000)));
775 drop(result);
776
777 let guard = remove_pid(&mut file, 2000).await.unwrap();
778 assert!(guard.is_none());
779 drop(guard);
780
781 let guard = remove_pid(&mut file, 1000).await.unwrap();
782 assert!(guard.is_none());
783 drop(guard);
784
785 let guard = remove_pid(&mut file, 3000).await.unwrap();
786 assert!(guard.is_some());
787 }
788
789 #[tokio::test]
791 async fn lock_file_remove_pid() {
792 let mut file = File::from_std(tempfile().unwrap());
793
794 let AddPidResult::OwnPid(guard) = add_pid(&mut file, 1234).await.unwrap() else {
795 panic!("expected OwnPid");
796 };
797 let file = guard.unlock().unwrap();
798
799 let mut guard = remove_pid(file, 1234).await.unwrap().unwrap();
800 let pids = read_pids(&mut guard).await.unwrap();
801 assert!(pids.is_empty());
802 }
803
804 #[tokio::test]
806 async fn lock_file_pids_sorted() {
807 let mut file = File::from_std(tempfile().unwrap());
808
809 let result = add_pid(&mut file, 3000).await.unwrap();
810 drop(result);
811 let result = add_pid(&mut file, 1000).await.unwrap();
812 drop(result);
813 let result = add_pid(&mut file, 2000).await.unwrap();
814 drop(result);
815
816 let mut guard = FileLockGuard::lock(&mut file).await.unwrap();
817 let pids = read_pids(&mut guard).await.unwrap();
818 assert_eq!(pids, vec![1000, 2000, 3000]);
819 }
820
821 #[tokio::test]
823 async fn lock_file_lock() {
824 let file = NamedTempFile::new().unwrap();
828 let mut file1 = File::options()
829 .read(true)
830 .write(true)
831 .open(file.path())
832 .await
833 .unwrap();
834 let AddPidResult::OwnPid(_guard1) = add_pid(&mut file1, 1000).await.unwrap() else {
835 panic!("expected OwnPid");
836 };
837
838 let mut file2 = File::options()
839 .read(true)
840 .write(true)
841 .open(file.path())
842 .await
843 .unwrap();
844 let add = add_pid(&mut file2, 2000);
845 let timeout = sleep(Duration::from_millis(10));
846
847 select! {
848 result = add => panic!("should not be able to add PID but got: {result:?}"),
849 () = timeout => (),
850 }
851 }
852
853 #[tokio::test]
855 async fn with_ref_setup_failure() {
856 let file = NamedTempFile::new().unwrap();
857 let path = file.path();
858
859 let setup = || async { Err(anyhow!("setup fail")) };
860 let body = |_ns_pid| async { unreachable!() };
861 let cleanup = |_is_first_user| async { unreachable!() };
862
863 let result = with_ref(path, setup, body, cleanup).await;
864 assert_eq!(result.unwrap_err().to_string(), "setup fail");
865 assert!(!try_exists(path).await.unwrap());
866 }
867
868 #[tokio::test]
870 async fn with_ref_body_failure() {
871 let file = NamedTempFile::new().unwrap();
872 let path = file.path();
873
874 let setup = || async { Ok(()) };
875 let body = |_ns_pid| async { Err(anyhow!("body fail")) };
876 let cleanup = |_is_first_user| async { Ok(()) };
877
878 let result = with_ref(path, setup, body, cleanup).await;
879 assert_eq!(result.unwrap_err().to_string(), "body fail");
880 assert!(!try_exists(path).await.unwrap());
881 }
882
883 #[tokio::test]
886 async fn with_ref_body_cleanup_failure() {
887 let file = NamedTempFile::new().unwrap();
888 let path = file.path();
889
890 let setup = || async { Ok(()) };
891 let body = |_ns_pid| async { Err(anyhow!("body fail")) };
892 let cleanup = |_is_first_user| async { Err(anyhow!("cleanup fail")) };
893
894 let result = with_ref(path, setup, body, cleanup).await;
895 assert_eq!(result.unwrap_err().to_string(), "body fail");
896 assert!(!try_exists(path).await.unwrap());
897 }
898
899 #[tokio::test]
901 async fn with_ref_cleanup_failure() {
902 let file = NamedTempFile::new().unwrap();
903 let path = file.path();
904
905 let setup = || async { Ok(()) };
906 let body = |_ns_pid| async { Ok(()) };
907 let cleanup = |_is_first_user| async { Err(anyhow!("cleanup fail")) };
908
909 let result = with_ref(path, setup, body, cleanup).await;
910 assert_eq!(result.unwrap_err().to_string(), "cleanup fail");
911 assert!(!try_exists(path).await.unwrap());
912 }
913
914 #[tokio::test]
916 async fn version() {
917 let args = [OsStr::new("chroot-deploy"), OsStr::new("--version")];
918 let () = run(args).await.unwrap();
919 }
920
921 #[tokio::test]
923 async fn help() {
924 let args = [OsStr::new("chroot-deploy"), OsStr::new("--help")];
925 let () = run(args).await.unwrap();
926 }
927
928 #[tokio::test]
931 async fn no_args() {
932 let args = [OsStr::new("chroot-deploy")];
933 let () = run(args).await.unwrap();
934 }
935}