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    /// Marks this container as a **reuse** sandbox — one meant to outlive this
89    /// process's own lifecycle rather than be torn down by it. Defaults to `false`;
90    /// no builder in this crate sets it yet (reuse itself is a later wave), but every
91    /// own-run cleanup path (the reaping ledger's `.sandboxes` file, the msb backend's
92    /// `started_names`, the docker backend's run-id label, this crate's `Drop`-path
93    /// cleanup) already knows to leave a `keep_alive` spec's container alone, so
94    /// wiring the field in now costs nothing and the reuse wave doesn't need to touch
95    /// any of those call sites again.
96    pub keep_alive: bool,
97    /// Set by [`crate::Container::from_checkpoint`] to the source [`crate::Checkpoint`]'s
98    /// `ref` — the checkpoint feature's own signal to the backend that this spec's
99    /// `image` is a checkpoint reference, not an ordinary image. docker ignores this
100    /// (the ref already IS a normal image tag; the ordinary create path just works);
101    /// microsandbox, when this is set, boots via `msb run --snapshot <ref>`
102    /// instead of its normal image boot, keeping every other flag identical.
103    /// Deliberately NOT part of the reuse identity hash — reuse and
104    /// `from_checkpoint` are not a supported combination (see
105    /// `RightsizeError::ReuseCheckpointConflict`). Defaults to `None`.
106    pub checkpoint_ref: Option<String>,
107}
108
109impl ContainerSpec {
110    /// Builds a spec with every optional field at its default (no env, no command, no
111    /// ports, no mounts, no network, no aliases, no memory limit).
112    pub fn new(
113        name: impl Into<String>,
114        image: impl Into<String>,
115        run_id: impl Into<String>,
116    ) -> Self {
117        Self {
118            name: name.into(),
119            image: image.into(),
120            env: Vec::new(),
121            command: None,
122            ports: Vec::new(),
123            mounts: Vec::new(),
124            network_id: None,
125            aliases: Vec::new(),
126            run_id: run_id.into(),
127            memory_limit_mb: None,
128            keep_alive: false,
129            checkpoint_ref: None,
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn container_spec_new_defaults_every_optional_field() {
140        let spec = ContainerSpec::new("rz-deadbeef-0", "redis:8.6-alpine", "deadbeef");
141        assert_eq!(spec.name, "rz-deadbeef-0");
142        assert_eq!(spec.image, "redis:8.6-alpine");
143        assert_eq!(spec.run_id, "deadbeef");
144        assert!(spec.env.is_empty());
145        assert!(spec.command.is_none());
146        assert!(spec.ports.is_empty());
147        assert!(spec.mounts.is_empty());
148        assert!(spec.network_id.is_none());
149        assert!(spec.aliases.is_empty());
150        assert_eq!(spec.memory_limit_mb, None);
151        assert!(!spec.keep_alive);
152        assert!(spec.checkpoint_ref.is_none());
153    }
154
155    #[test]
156    fn file_mount_defaults_to_read_only() {
157        let m = FileMount::new("/host/f.txt", "/guest/f.txt");
158        assert!(m.read_only);
159        assert_eq!(m.host_path, PathBuf::from("/host/f.txt"));
160        assert_eq!(m.guest_path, "/guest/f.txt");
161    }
162
163    #[test]
164    fn file_mount_read_write_flips_the_flag() {
165        let m = FileMount::new("/host/f.txt", "/guest/f.txt").read_write();
166        assert!(!m.read_only);
167    }
168
169    #[test]
170    fn port_binding_and_exec_result_are_plain_value_types() {
171        let a = PortBinding {
172            host_port: 32768,
173            guest_port: 6379,
174        };
175        let b = a.clone();
176        assert_eq!(a, b);
177
178        let r = ExecResult {
179            exit_code: 0,
180            stdout: "ok".into(),
181            stderr: String::new(),
182        };
183        assert_eq!(r.exit_code, 0);
184        assert_eq!(r.stdout, "ok");
185    }
186}