1#![doc = include_str!("../README.md")]
2
3pub mod backend;
4pub mod image;
5pub mod platform;
6pub mod reference;
7pub mod testing;
8
9pub use backend::{Backend, BridgeSubnetError, ContainerHostnameResolver, ResolveHostnameError};
10use cmd_proc::Command;
11use cmd_proc::CommandError;
12pub use image::{
13 BuildArgumentKey, BuildArgumentKeyError, BuildArgumentValue, BuildDefinition, BuildSource,
14 BuildTarget, Reference,
15};
16
17trait Apply {
18 fn apply(&self, command: Command) -> Command;
19}
20
21impl<T: Apply> Apply for Vec<T> {
22 fn apply(&self, command: Command) -> Command {
23 self.iter()
24 .fold(command, |command, item| item.apply(command))
25 }
26}
27
28impl<T: Apply> Apply for Option<T> {
29 fn apply(&self, command: Command) -> Command {
30 match self {
31 Some(item) => item.apply(command),
32 None => command,
33 }
34 }
35}
36
37macro_rules! string_newtype {
39 ($name:ident) => {
40 impl From<String> for $name {
41 fn from(value: String) -> Self {
42 Self(value)
43 }
44 }
45
46 impl From<&str> for $name {
47 fn from(value: &str) -> Self {
48 Self(value.to_string())
49 }
50 }
51
52 impl AsRef<std::ffi::OsStr> for $name {
53 fn as_ref(&self) -> &std::ffi::OsStr {
54 self.0.as_ref()
55 }
56 }
57
58 impl $name {
59 pub fn as_str(&self) -> &str {
60 &self.0
61 }
62 }
63 };
64}
65
66macro_rules! apply_argument {
68 ($name:ident, $flag:expr) => {
69 string_newtype!($name);
70
71 impl Apply for $name {
72 fn apply(&self, command: Command) -> Command {
73 command.argument($flag).argument(self)
74 }
75 }
76 };
77}
78
79#[derive(Clone, Debug, Eq, PartialEq)]
80pub struct ContainerArgument(String);
81
82string_newtype!(ContainerArgument);
83
84impl Apply for ContainerArgument {
85 fn apply(&self, command: Command) -> Command {
86 command.argument(self)
87 }
88}
89
90impl Apply for image::Reference {
91 fn apply(&self, command: Command) -> Command {
92 command.argument(self.to_string())
93 }
94}
95
96#[derive(Clone, Debug, Eq, PartialEq)]
97pub enum Detach {
98 Detach,
99 NoDetach,
100}
101
102impl Apply for Detach {
103 fn apply(&self, command: Command) -> Command {
104 match self {
105 Self::Detach => command.argument("--detach"),
106 Self::NoDetach => command,
107 }
108 }
109}
110
111#[derive(Clone, Debug, Eq, PartialEq)]
112pub enum Remove {
113 Remove,
114 NoRemove,
115}
116
117impl Apply for Remove {
118 fn apply(&self, command: Command) -> Command {
119 match self {
120 Self::Remove => command.argument("--rm"),
121 Self::NoRemove => command,
122 }
123 }
124}
125
126#[derive(Clone, Debug, Eq, PartialEq)]
127pub struct Mount(String);
128
129apply_argument!(Mount, "--mount");
130
131const UNSPECIFIED_IP: std::net::IpAddr = std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED);
132
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134pub enum Protocol {
135 Tcp,
136 Udp,
137}
138
139impl Protocol {
140 fn as_str(&self) -> &'static str {
141 match self {
142 Self::Tcp => "tcp",
143 Self::Udp => "udp",
144 }
145 }
146}
147
148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
149struct HostBinding {
150 ip: std::net::IpAddr,
151 port: Option<u16>,
152}
153
154impl std::fmt::Display for HostBinding {
155 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 write!(formatter, "{}:", self.ip)?;
157
158 if let Some(port) = self.port {
159 write!(formatter, "{port}")
160 } else {
161 Ok(())
162 }
163 }
164}
165
166#[derive(Clone, Debug, Eq, PartialEq)]
171pub struct Publish {
172 host_binding: Option<HostBinding>,
173 container_port: u16,
174 protocol: Protocol,
175}
176
177impl Publish {
178 #[must_use]
187 pub fn tcp(container_port: u16) -> Self {
188 Self {
189 host_binding: None,
190 container_port,
191 protocol: Protocol::Tcp,
192 }
193 }
194
195 #[must_use]
204 pub fn udp(container_port: u16) -> Self {
205 Self {
206 host_binding: None,
207 container_port,
208 protocol: Protocol::Udp,
209 }
210 }
211
212 #[must_use]
247 pub fn host_ip(self, ip: std::net::IpAddr) -> Self {
248 Self {
249 host_binding: Some(HostBinding {
250 ip,
251 port: self.host_binding.and_then(|binding| binding.port),
252 }),
253 ..self
254 }
255 }
256
257 #[must_use]
277 pub fn host_port(self, port: u16) -> Self {
278 Self {
279 host_binding: Some(HostBinding {
280 ip: self
281 .host_binding
282 .map(|binding| binding.ip)
283 .unwrap_or(UNSPECIFIED_IP),
284 port: Some(port),
285 }),
286 ..self
287 }
288 }
289
290 #[must_use]
300 pub fn host_ip_port(self, ip: std::net::IpAddr, port: u16) -> Self {
301 Self {
302 host_binding: Some(HostBinding {
303 ip,
304 port: Some(port),
305 }),
306 ..self
307 }
308 }
309}
310
311impl std::fmt::Display for Publish {
312 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313 if let Some(host_binding) = self.host_binding {
314 write!(formatter, "{host_binding}:")?;
315 }
316
317 write!(
318 formatter,
319 "{}/{}",
320 self.container_port,
321 self.protocol.as_str()
322 )
323 }
324}
325
326impl Apply for Publish {
327 fn apply(&self, command: Command) -> Command {
328 command.argument("--publish").argument(self.to_string())
329 }
330}
331
332#[derive(Clone, Debug, Eq, PartialEq)]
333pub struct Entrypoint(String);
334
335apply_argument!(Entrypoint, "--entrypoint");
336
337#[derive(Clone, Debug, Eq, PartialEq)]
338pub struct Workdir(String);
339
340apply_argument!(Workdir, "--workdir");
341
342#[derive(Clone, Debug, Eq, PartialEq)]
343pub struct EnvironmentVariables(
344 std::collections::BTreeMap<cmd_proc::EnvVariableName<'static>, String>,
345);
346
347impl EnvironmentVariables {
348 fn new() -> Self {
349 Self(std::collections::BTreeMap::new())
350 }
351
352 fn insert(&mut self, key: cmd_proc::EnvVariableName<'static>, value: String) {
353 self.0.insert(key, value);
354 }
355}
356
357impl Apply for EnvironmentVariables {
358 fn apply(&self, command: Command) -> Command {
359 self.0.iter().fold(command, |command, (key, value)| {
360 command.argument("--env").argument(format!("{key}={value}"))
361 })
362 }
363}
364
365#[derive(Clone, Debug, Eq, PartialEq)]
366pub struct Definition {
367 backend: Backend,
368 container_arguments: Vec<ContainerArgument>,
369 detach: Detach,
370 entrypoint: Option<Entrypoint>,
371 environment_variables: EnvironmentVariables,
372 reference: image::Reference,
373 remove: Remove,
374 mounts: Vec<Mount>,
375 publish: Vec<Publish>,
376 workdir: Option<Workdir>,
377}
378
379impl Definition {
380 #[must_use]
381 pub fn new(backend: Backend, reference: image::Reference) -> Definition {
382 Definition {
383 backend,
384 container_arguments: vec![],
385 detach: Detach::NoDetach,
386 entrypoint: None,
387 environment_variables: EnvironmentVariables::new(),
388 reference,
389 mounts: vec![],
390 publish: vec![],
391 remove: Remove::NoRemove,
392 workdir: None,
393 }
394 }
395
396 pub async fn with_container<R>(&self, mut action: impl AsyncFnMut(&mut Container) -> R) -> R {
400 let mut container = self.clone().run_detached().await;
401 let result = action(&mut container).await;
402 container.stop().await;
403 result
404 }
405
406 #[must_use]
407 pub fn backend(self, backend: Backend) -> Self {
408 Self { backend, ..self }
409 }
410
411 pub fn entrypoint(self, command: impl Into<Entrypoint>) -> Self {
412 Self {
413 entrypoint: Some(command.into()),
414 ..self
415 }
416 }
417
418 pub fn workdir(self, path: impl Into<Workdir>) -> Self {
419 Self {
420 workdir: Some(path.into()),
421 ..self
422 }
423 }
424
425 pub fn arguments(
426 self,
427 arguments: impl IntoIterator<Item = impl Into<ContainerArgument>>,
428 ) -> Self {
429 Self {
430 container_arguments: arguments.into_iter().map(Into::into).collect(),
431 ..self
432 }
433 }
434
435 pub fn argument(self, argument: impl Into<ContainerArgument>) -> Self {
436 let mut container_arguments = self.container_arguments;
437 container_arguments.push(argument.into());
438 Self {
439 container_arguments,
440 ..self
441 }
442 }
443
444 #[must_use]
446 pub fn environment_variable(
447 self,
448 key: cmd_proc::EnvVariableName<'static>,
449 value: &str,
450 ) -> Self {
451 let mut environment_variables = self.environment_variables;
452
453 environment_variables.insert(key, value.to_string());
454
455 Self {
456 environment_variables,
457 ..self
458 }
459 }
460
461 pub fn environment_variables<V: Into<String>>(
463 self,
464 values: impl IntoIterator<Item = (cmd_proc::EnvVariableName<'static>, V)>,
465 ) -> Self {
466 let mut environment_variables = self.environment_variables;
467
468 for (key, value) in values {
469 environment_variables.insert(key, value.into());
470 }
471
472 Self {
473 environment_variables,
474 ..self
475 }
476 }
477
478 #[must_use]
479 pub fn remove(self) -> Self {
480 Self {
481 remove: Remove::Remove,
482 ..self
483 }
484 }
485
486 #[must_use]
487 pub fn no_remove(self) -> Self {
488 Self {
489 remove: Remove::NoRemove,
490 ..self
491 }
492 }
493
494 #[must_use]
495 pub fn detach(self) -> Self {
496 Self {
497 detach: Detach::Detach,
498 ..self
499 }
500 }
501
502 #[must_use]
503 pub fn no_detach(self) -> Self {
504 Self {
505 detach: Detach::NoDetach,
506 ..self
507 }
508 }
509
510 pub fn publish(self, value: impl Into<Publish>) -> Self {
511 let mut publish = self.publish;
512
513 publish.push(value.into());
514
515 Self { publish, ..self }
516 }
517
518 pub fn publishes(self, values: impl IntoIterator<Item = impl Into<Publish>>) -> Self {
519 let mut publish = self.publish;
520 publish.extend(values.into_iter().map(Into::into));
521 Self { publish, ..self }
522 }
523
524 pub fn mount(self, value: impl Into<Mount>) -> Self {
525 let mut mounts = self.mounts;
526
527 mounts.push(value.into());
528
529 Self { mounts, ..self }
530 }
531
532 pub fn mounts(self, values: impl IntoIterator<Item = impl Into<Mount>>) -> Self {
533 let mut mounts = self.mounts;
534 mounts.extend(values.into_iter().map(Into::into));
535 Self { mounts, ..self }
536 }
537
538 pub async fn run_detached(&self) -> Container {
539 let stdout = self.clone().detach().run_output().await;
540
541 Container {
542 backend: self.backend.clone(),
543 id: ContainerId::try_from(strip_nl_end(&stdout)).unwrap(),
544 }
545 }
546
547 pub async fn run_capture_only_stdout(&self) -> Vec<u8> {
548 self.clone().no_detach().run_output().await
549 }
550
551 pub async fn run(&self) -> Result<(), CommandError> {
553 self.build_run_command().status().await
554 }
555
556 fn build_run_command(&self) -> Command {
557 let command = self.backend.command().argument("run");
558
559 let command = self.detach.apply(command);
560 let command = self.remove.apply(command);
561 let command = self.environment_variables.apply(command);
562 let command = self.publish.apply(command);
563 let command = self.mounts.apply(command);
564 let command = self.workdir.apply(command);
565 let command = self.entrypoint.apply(command);
566 let command = self.reference.apply(command);
567
568 self.container_arguments.apply(command)
569 }
570
571 async fn run_output(&self) -> Vec<u8> {
572 self.build_run_command()
573 .stdout_capture()
574 .bytes()
575 .await
576 .unwrap()
577 }
578}
579
580fn strip_nl_end(value: &[u8]) -> &[u8] {
581 match value.split_last() {
582 Some((last, prefix)) => {
583 if *last == b'\n' {
584 prefix
585 } else {
586 panic!("last byte not a newline")
587 }
588 }
589 None => panic!("empty slice"),
590 }
591}
592
593#[derive(Clone, Debug, Eq, PartialEq)]
594pub struct ContainerId(String);
595
596impl std::convert::TryFrom<&[u8]> for ContainerId {
597 type Error = std::str::Utf8Error;
598
599 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
600 std::str::from_utf8(value).map(|str| ContainerId(str.to_string()))
601 }
602}
603
604impl AsRef<std::ffi::OsStr> for ContainerId {
605 fn as_ref(&self) -> &std::ffi::OsStr {
606 self.0.as_ref()
607 }
608}
609
610impl ContainerId {
611 #[must_use]
612 pub fn as_str(&self) -> &str {
613 &self.0
614 }
615}
616
617#[derive(Debug)]
618pub struct Container {
619 backend: Backend,
620 id: ContainerId,
621}
622
623pub struct ExecCommand<'a> {
625 container: &'a Container,
626 executable: String,
627 arguments: Vec<String>,
628 environment: Vec<(cmd_proc::EnvVariableName<'static>, String)>,
629 interactive: bool,
630 stdin_data: Option<Vec<u8>>,
631}
632
633impl<'a> ExecCommand<'a> {
634 fn new(container: &'a Container, executable: impl Into<String>) -> Self {
635 Self {
636 container,
637 executable: executable.into(),
638 arguments: Vec::new(),
639 environment: Vec::new(),
640 interactive: false,
641 stdin_data: None,
642 }
643 }
644
645 pub fn argument(mut self, value: impl Into<String>) -> Self {
647 self.arguments.push(value.into());
648 self
649 }
650
651 pub fn arguments(mut self, values: impl IntoIterator<Item = impl Into<String>>) -> Self {
653 self.arguments.extend(values.into_iter().map(Into::into));
654 self
655 }
656
657 pub fn environment_variable(
661 mut self,
662 key: cmd_proc::EnvVariableName<'static>,
663 value: impl Into<String>,
664 ) -> Self {
665 self.environment.push((key, value.into()));
666 self
667 }
668
669 pub fn environment_variables(
673 mut self,
674 variables: impl IntoIterator<Item = (cmd_proc::EnvVariableName<'static>, impl Into<String>)>,
675 ) -> Self {
676 self.environment.extend(
677 variables
678 .into_iter()
679 .map(|(key, value)| (key, value.into())),
680 );
681 self
682 }
683
684 #[must_use]
686 pub fn interactive(mut self) -> Self {
687 self.interactive = true;
688 self
689 }
690
691 pub fn stdin(mut self, data: impl Into<Vec<u8>>) -> Self {
693 self.stdin_data = Some(data.into());
694 self
695 }
696
697 #[must_use]
701 pub fn build(self) -> Command {
702 let mut command = self.container.backend_command().argument("exec");
703
704 if self.interactive {
705 command = command.argument("--tty").argument("--interactive");
706 } else if self.stdin_data.is_some() {
707 command = command.argument("--interactive");
708 }
709
710 for (key, value) in self.environment {
711 command = command.argument("--env").argument(format!("{key}={value}"));
712 }
713
714 command = command
715 .argument(&self.container.id)
716 .argument(self.executable)
717 .arguments(self.arguments);
718
719 if let Some(data) = self.stdin_data {
720 command = command.stdin_bytes(data);
721 }
722
723 command
724 }
725
726 pub async fn status(self) -> Result<(), CommandError> {
728 self.build().status().await
729 }
730}
731
732impl Container {
733 pub fn exec(&self, executable: impl Into<String>) -> ExecCommand<'_> {
735 ExecCommand::new(self, executable)
736 }
737
738 pub async fn stop(&mut self) {
739 self.backend_command()
740 .arguments(["container", "stop"])
741 .argument(&self.id)
742 .stdout_capture()
743 .bytes()
744 .await
745 .unwrap();
746 }
747
748 pub async fn remove(&mut self) {
749 self.backend_command()
750 .arguments(["container", "rm"])
751 .argument(&self.id)
752 .stdout_capture()
753 .bytes()
754 .await
755 .unwrap();
756 }
757
758 pub async fn inspect(&self) -> serde_json::Value {
759 let stdout = self
760 .backend_command()
761 .argument("inspect")
762 .argument(&self.id)
763 .stdout_capture()
764 .bytes()
765 .await
766 .unwrap();
767
768 serde_json::from_slice(&stdout).expect("invalid json")
769 }
770
771 pub async fn inspect_format(&self, format: &str) -> String {
772 let bytes = self
773 .backend_command()
774 .argument("inspect")
775 .argument("--format")
776 .argument(format)
777 .argument(&self.id)
778 .stdout_capture()
779 .bytes()
780 .await
781 .unwrap();
782
783 std::str::from_utf8(strip_nl_end(&bytes))
784 .expect("invalid utf8")
785 .to_string()
786 }
787
788 pub async fn read_host_tcp_port(&self, container_port: u16) -> Option<u16> {
789 let json = self.inspect().await;
790
791 json.get(0)?
792 .get("NetworkSettings")?
793 .get("Ports")?
794 .get(format!("{container_port}/tcp"))?
795 .get(0)?
796 .get("HostPort")?
797 .as_str()?
798 .parse()
799 .ok()
800 }
801
802 pub async fn commit(
803 &self,
804 reference: &image::Reference,
805 pause: bool,
806 ) -> Result<(), CommandError> {
807 let pause_argument = match (&self.backend, pause) {
808 (Backend::Docker { .. }, true) => None,
809 (Backend::Docker { version }, false) => {
810 if version.major >= 29 {
813 Some("--no-pause")
814 } else {
815 Some("--pause=false")
816 }
817 }
818 (Backend::Podman { .. }, true) => Some("--pause"),
819 (Backend::Podman { .. }, false) => None,
820 };
821
822 self.backend_command()
823 .argument("commit")
824 .optional_argument(pause_argument)
825 .argument(&self.id)
826 .argument(reference.to_string())
827 .status()
828 .await
829 }
830
831 fn backend_command(&self) -> Command {
832 self.backend.command()
833 }
834}