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}