Skip to main content

rightsize/
model.rs

1//! The container model: the plain-data types a backend needs to create, describe, and
2//! mount things into a container. `ContainerSpec` in particular carries **already
3//! chosen** host ports — see [`ContainerSpec::ports`] — a backend binds them, it never
4//! allocates.
5
6use std::path::PathBuf;
7
8/// A host↔guest port map entry. The runtime binds `host_port` on loopback and forwards
9/// traffic to `guest_port` inside the container.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct PortBinding {
12    /// The host-side port, already chosen by the core allocator before the backend ever
13    /// sees this spec.
14    pub host_port: u16,
15    /// The port the workload listens on inside the guest.
16    pub guest_port: u16,
17}
18
19/// A host file or directory exposed inside the guest at `guest_path`.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct FileMount {
22    /// The host-side path being mounted in.
23    pub host_path: PathBuf,
24    /// The absolute path the file appears at inside the guest.
25    pub guest_path: String,
26    /// Whether the guest may only read it. Defaults to `true` — mounts are read-only
27    /// unless a caller opts into read-write explicitly.
28    pub read_only: bool,
29}
30
31impl FileMount {
32    /// Builds a `FileMount` with the default `read_only: true`.
33    pub fn new(host_path: impl Into<PathBuf>, guest_path: impl Into<String>) -> Self {
34        Self {
35            host_path: host_path.into(),
36            guest_path: guest_path.into(),
37            read_only: true,
38        }
39    }
40
41    /// Returns a copy of this mount with `read_only` set to `false`.
42    pub fn read_write(mut self) -> Self {
43        self.read_only = false;
44        self
45    }
46}
47
48/// The outcome of a single `exec` call against a running container.
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct ExecResult {
51    /// The process exit code (0 = success).
52    pub exit_code: i32,
53    /// Everything written to stdout.
54    pub stdout: String,
55    /// Everything written to stderr.
56    pub stderr: String,
57}
58
59/// Everything a backend needs to create one container. Host ports in `ports` are
60/// **already chosen** by the core allocator (see `free_ports`) — a backend binds them,
61/// it never allocates, so the same spec works identically whether the backend is a
62/// microVM or a Docker daemon.
63#[derive(Clone, Debug)]
64pub struct ContainerSpec {
65    /// The backend-facing container name, e.g. `rz-<run_id>-<seq>`.
66    pub name: String,
67    /// The image reference to run.
68    pub image: String,
69    /// Environment variables, insertion-ordered — a `Vec` of pairs rather than a
70    /// `HashMap` so order and duplicate-key handling stay under the caller's control.
71    pub env: Vec<(String, String)>,
72    /// The command to run instead of the image's default entrypoint. `None` means "run
73    /// the image as built."
74    pub command: Option<Vec<String>>,
75    /// Already-chosen host↔guest port bindings — see the type-level doc.
76    pub ports: Vec<PortBinding>,
77    /// Host files/directories to mount into the guest.
78    pub mounts: Vec<FileMount>,
79    /// The network to join, if any.
80    pub network_id: Option<String>,
81    /// DNS-style aliases this container is reachable as by other members of its network.
82    pub aliases: Vec<String>,
83    /// The per-process run id (see `run_id`), used to label/name containers so a crashed
84    /// run's leftovers can be told apart from a live run's.
85    pub run_id: String,
86    /// An optional memory cap in megabytes.
87    pub memory_limit_mb: Option<u64>,
88}
89
90impl ContainerSpec {
91    /// Builds a spec with every optional field at its default (no env, no command, no
92    /// ports, no mounts, no network, no aliases, no memory limit).
93    pub fn new(
94        name: impl Into<String>,
95        image: impl Into<String>,
96        run_id: impl Into<String>,
97    ) -> Self {
98        Self {
99            name: name.into(),
100            image: image.into(),
101            env: Vec::new(),
102            command: None,
103            ports: Vec::new(),
104            mounts: Vec::new(),
105            network_id: None,
106            aliases: Vec::new(),
107            run_id: run_id.into(),
108            memory_limit_mb: None,
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn container_spec_new_defaults_every_optional_field() {
119        let spec = ContainerSpec::new("rz-deadbeef-0", "redis:8.6-alpine", "deadbeef");
120        assert_eq!(spec.name, "rz-deadbeef-0");
121        assert_eq!(spec.image, "redis:8.6-alpine");
122        assert_eq!(spec.run_id, "deadbeef");
123        assert!(spec.env.is_empty());
124        assert!(spec.command.is_none());
125        assert!(spec.ports.is_empty());
126        assert!(spec.mounts.is_empty());
127        assert!(spec.network_id.is_none());
128        assert!(spec.aliases.is_empty());
129        assert_eq!(spec.memory_limit_mb, None);
130    }
131
132    #[test]
133    fn file_mount_defaults_to_read_only() {
134        let m = FileMount::new("/host/f.txt", "/guest/f.txt");
135        assert!(m.read_only);
136        assert_eq!(m.host_path, PathBuf::from("/host/f.txt"));
137        assert_eq!(m.guest_path, "/guest/f.txt");
138    }
139
140    #[test]
141    fn file_mount_read_write_flips_the_flag() {
142        let m = FileMount::new("/host/f.txt", "/guest/f.txt").read_write();
143        assert!(!m.read_only);
144    }
145
146    #[test]
147    fn port_binding_and_exec_result_are_plain_value_types() {
148        let a = PortBinding {
149            host_port: 32768,
150            guest_port: 6379,
151        };
152        let b = a.clone();
153        assert_eq!(a, b);
154
155        let r = ExecResult {
156            exit_code: 0,
157            stdout: "ok".into(),
158            stderr: String::new(),
159        };
160        assert_eq!(r.exit_code, 0);
161        assert_eq!(r.stdout, "ok");
162    }
163}