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}