| 1 | //! How the runner keeps work from the machine it is on, and what it checks |
| 2 | //! before it takes any. |
| 3 | //! |
| 4 | //! The goal is two walls: the runner in a sandbox of its own, away from |
| 5 | //! its machine, and each job in a fresh sandbox of its own inside that, |
| 6 | //! made for the job and destroyed after. Where the runner stands today: |
| 7 | //! |
| 8 | //! | Isolation | How | Wall from the machine | |
| 9 | //! | --- | --- | --- | |
| 10 | //! | `nested-container` | The runner is a container under Sysbox (`docker run --runtime=sysbox-runc`), with its own Docker Engine inside; each job is a container on that engine. | Two: the job's container, then the runner's, whose root is no one on the machine. | |
| 11 | //! | `container` | The runner drives a Docker Engine it shares with the machine (installed on it, or a container with the machine's Docker socket); each job is a container on it. | One: the job's container. The runner itself can do anything the machine's Docker can, which is everything. | |
| 12 | //! | `host` | `--no-docker`: each job is a process on the machine, in a folder of its own. Deprecated. | None. | |
| 13 | //! |
| 14 | //! [`SandboxDriver`] is what makes a job's sandbox. [`NestedDocker`] (and |
| 15 | //! [`SharedDocker`], the same on a shared engine) is built: exec.rs runs |
| 16 | //! jobs through it. [`HyperVContainer`] and [`MacVm`] are the designs for |
| 17 | //! Windows and macOS, declared so the shape is fixed, and not built. |
| 18 | |
| 19 | use std::path::Path; |
| 20 | |
| 21 | use anyhow::{Result, bail}; |
| 22 | |
| 23 | /// How well a runner keeps work from its machine, as it tells g1t when it |
| 24 | /// registers (`isolation`). |
| 25 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 26 | pub enum Isolation { |
| 27 | /// A container per job, inside the runner's own sandboxed container. |
| 28 | NestedContainer, |
| 29 | /// A container per job, on a Docker Engine shared with the machine. |
| 30 | Container, |
| 31 | /// Directly on the machine. |
| 32 | Host, |
| 33 | } |
| 34 | |
| 35 | impl Isolation { |
| 36 | pub fn as_str(self) -> &'static str { |
| 37 | match self { |
| 38 | Isolation::NestedContainer => "nested-container", |
| 39 | Isolation::Container => "container", |
| 40 | Isolation::Host => "host", |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | /// For the log at start. |
| 45 | pub fn describe(self) -> &'static str { |
| 46 | match self { |
| 47 | Isolation::NestedContainer => { |
| 48 | "nested-container: each job runs in a fresh container on this runner's own Docker Engine, inside the runner's sandboxed container; neither reaches the machine" |
| 49 | } |
| 50 | Isolation::Container => { |
| 51 | "container: each job runs in a fresh container, but the runner is not sandboxed from the machine (it shares the machine's Docker Engine, or its container's root is the machine's), so the runner, and anything that takes it over, controls the machine. Run the runner's image under Sysbox for nested-container isolation" |
| 52 | } |
| 53 | Isolation::Host => "host: jobs run directly on this machine, unsandboxed", |
| 54 | } |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | /// What `G1T_RUNNER_SANDBOX` asks for: `nested`, the runner's image's |
| 59 | /// setting, starts a Docker Engine inside the runner's container. |
| 60 | pub const SANDBOX_ENV: &str = "G1T_RUNNER_SANDBOX"; |
| 61 | |
| 62 | /// The override for the checks below, loudly named: with it, a runner |
| 63 | /// whose jobs can reach the machine starts anyway. |
| 64 | pub const UNSAFE_ENV: &str = "G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS"; |
| 65 | |
| 66 | pub fn nested_requested() -> bool { |
| 67 | std::env::var(SANDBOX_ENV).is_ok_and(|v| v.eq_ignore_ascii_case("nested")) |
| 68 | } |
| 69 | |
| 70 | pub fn unsafe_allowed() -> bool { |
| 71 | std::env::var(UNSAFE_ENV).is_ok_and(|v| matches!(v.as_str(), "1" | "true" | "yes")) |
| 72 | } |
| 73 | |
| 74 | // --- Host mode's deprecation --------------------------------------------- |
| 75 | |
| 76 | /// What the runner's log says when it starts in host mode. |
| 77 | pub const HOST_MODE_AT_START: &str = "Deprecated: this runner runs jobs directly on this machine (--no-docker), unsandboxed. Every job can read, change and leave behind anything this user can. Host mode will be removed: run the runner's image under Sysbox instead, see https://docs.g1t.sh/guides/self-hosted-runners/#isolation"; |
| 78 | |
| 79 | /// What every job's log says when it ran in host mode. |
| 80 | pub const HOST_MODE_IN_JOB: &str = "This job ran on the host, unsandboxed: the self-hosted runner runs jobs directly on its machine (--no-docker). Host mode will be removed; see https://docs.g1t.sh/guides/self-hosted-runners/#isolation"; |
| 81 | |
| 82 | /// Why a public repository's job is refused in host mode. |
| 83 | pub fn host_mode_refuses(repo: &str) -> String { |
| 84 | format!( |
| 85 | "{repo} is public, and this self-hosted runner runs jobs directly on its machine (--no-docker), unsandboxed, so it does not take a public repository's work. Run the runner's image under Sysbox, or register it without --no-docker. See https://docs.g1t.sh/guides/self-hosted-runners/#isolation" |
| 86 | ) |
| 87 | } |
| 88 | |
| 89 | // --- What the runner's container can see ---------------------------------- |
| 90 | |
| 91 | /// Socket names that are a container engine's: whoever has one controls |
| 92 | /// that engine, and through it the machine it runs on. |
| 93 | const ENGINE_SOCKETS: &[&str] = &["docker.sock", "containerd.sock", "podman.sock", "crio.sock", "dockershim.sock", "cri-dockerd.sock"]; |
| 94 | |
| 95 | fn is_engine_socket(path: &str) -> bool { |
| 96 | let name = path.rsplit('/').next().unwrap_or(path); |
| 97 | ENGINE_SOCKETS.contains(&name) |
| 98 | } |
| 99 | |
| 100 | /// mountinfo escapes spaces and the like as `\040`; undoes it. |
| 101 | fn unescape(field: &str) -> String { |
| 102 | let bytes = field.as_bytes(); |
| 103 | let mut out = Vec::with_capacity(bytes.len()); |
| 104 | let mut i = 0; |
| 105 | while i < bytes.len() { |
| 106 | if bytes[i] == b'\\' |
| 107 | && let Some(code) = bytes.get(i + 1..i + 4).and_then(|d| std::str::from_utf8(d).ok()).and_then(|d| u8::from_str_radix(d, 8).ok()) |
| 108 | { |
| 109 | out.push(code); |
| 110 | i += 4; |
| 111 | continue; |
| 112 | } |
| 113 | out.push(bytes[i]); |
| 114 | i += 1; |
| 115 | } |
| 116 | String::from_utf8_lossy(&out).into_owned() |
| 117 | } |
| 118 | |
| 119 | /// The engine sockets mounted into this container from outside it, from |
| 120 | /// `/proc/self/mountinfo`: a mount point, or the file a mount came from, |
| 121 | /// named like an engine's socket (`-v /var/run/docker.sock:/var/run/docker.sock`, |
| 122 | /// or the same socket under another name). |
| 123 | pub fn mounted_engine_sockets(mountinfo: &str) -> Vec<String> { |
| 124 | let mut found = Vec::new(); |
| 125 | for line in mountinfo.lines() { |
| 126 | // `id parent major:minor root mount-point options [optional…] - fstype source super-options` |
| 127 | let fields: Vec<&str> = line.split_whitespace().collect(); |
| 128 | if fields.len() < 5 { |
| 129 | continue; |
| 130 | } |
| 131 | let (root, mount_point) = (unescape(fields[3]), unescape(fields[4])); |
| 132 | if is_engine_socket(&mount_point) || is_engine_socket(&root) { |
| 133 | found.push(mount_point); |
| 134 | } |
| 135 | } |
| 136 | found |
| 137 | } |
| 138 | |
| 139 | /// Whether this container runs under Sysbox, whose virtual files |
| 140 | /// (`/proc/uptime`, `/sys/kernel`, …) come from `sysboxfs`. |
| 141 | pub fn under_sysbox(mountinfo: &str) -> bool { |
| 142 | mountinfo.lines().any(|line| line.split(" - ").nth(1).is_some_and(|rest| rest.split_whitespace().nth(1) == Some("sysboxfs"))) |
| 143 | } |
| 144 | |
| 145 | /// Whether root here is root on the machine: no user namespace, every id |
| 146 | /// mapped to itself (`/proc/self/uid_map` is `0 0 4294967295`). |
| 147 | pub fn root_is_the_machines(uid_map: &str) -> bool { |
| 148 | let rows: Vec<Vec<&str>> = uid_map.lines().map(|l| l.split_whitespace().collect()).filter(|r: &Vec<&str>| r.len() == 3).collect(); |
| 149 | rows.iter().any(|r| r[0] == "0" && r[1] == "0" && r[2].parse::<u64>().is_ok_and(|n| n >= 4_294_967_295)) |
| 150 | } |
| 151 | |
| 152 | /// What a nested runner found about its container. |
| 153 | #[derive(Debug, Default)] |
| 154 | pub struct Findings { |
| 155 | /// Engine sockets mounted in from outside. |
| 156 | pub sockets: Vec<String>, |
| 157 | /// An engine socket that is just there, where the runner's own engine |
| 158 | /// does not put one: a directory such as `/var/run` mounted from the |
| 159 | /// machine. |
| 160 | pub stray_sockets: Vec<String>, |
| 161 | /// `DOCKER_HOST` pointing somewhere. |
| 162 | pub docker_host: Option<String>, |
| 163 | pub sysbox: bool, |
| 164 | pub machine_root: bool, |
| 165 | } |
| 166 | |
| 167 | impl Findings { |
| 168 | pub fn read() -> Findings { |
| 169 | let mountinfo = std::fs::read_to_string("/proc/self/mountinfo").unwrap_or_default(); |
| 170 | let uid_map = std::fs::read_to_string("/proc/self/uid_map").unwrap_or_default(); |
| 171 | let mut findings = Findings::from_text(&mountinfo, &uid_map); |
| 172 | for path in ["/var/run/docker.sock", "/run/docker.sock", "/run/containerd/containerd.sock", "/run/podman/podman.sock"] { |
| 173 | // `/var/run` is often `/run`: the same file is said once. |
| 174 | let same = |a: &str, b: &str| a == b || std::fs::canonicalize(a).ok().zip(std::fs::canonicalize(b).ok()).is_some_and(|(a, b)| a == b); |
| 175 | if Path::new(path).exists() && !findings.sockets.iter().any(|s| same(s, path)) { |
| 176 | findings.stray_sockets.push(path.to_owned()); |
| 177 | } |
| 178 | } |
| 179 | findings.docker_host = std::env::var("DOCKER_HOST").ok().filter(|h| !h.is_empty()); |
| 180 | findings |
| 181 | } |
| 182 | |
| 183 | pub fn from_text(mountinfo: &str, uid_map: &str) -> Findings { |
| 184 | Findings { |
| 185 | sockets: mounted_engine_sockets(mountinfo), |
| 186 | sysbox: under_sysbox(mountinfo), |
| 187 | machine_root: root_is_the_machines(uid_map), |
| 188 | ..Findings::default() |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | /// What reaches the machine from here, one reason a line. |
| 193 | pub fn problems(&self) -> Vec<String> { |
| 194 | let mut problems = Vec::new(); |
| 195 | for socket in &self.sockets { |
| 196 | problems.push(format!("{socket} is a container engine's socket mounted in from outside this container: whoever uses it controls the machine")); |
| 197 | } |
| 198 | for socket in &self.stray_sockets { |
| 199 | problems.push(format!("{socket} is a container engine's socket that this runner did not make (a folder mounted from the machine?)")); |
| 200 | } |
| 201 | if let Some(host) = &self.docker_host { |
| 202 | problems.push(format!("DOCKER_HOST is set ({host}): jobs would run on that engine, not this runner's own")); |
| 203 | } |
| 204 | if self.machine_root && !self.sysbox { |
| 205 | problems.push("root in this container is root on the machine (it has no user namespace of its own): run it with --runtime=sysbox-runc, and without --privileged".to_owned()); |
| 206 | } |
| 207 | problems |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | /// Decides a nested runner's isolation, or refuses to start. With |
| 212 | /// [`UNSAFE_ENV`] it starts anyway, as `container`, and says why loudly. |
| 213 | pub fn check_nested(findings: &Findings, allow_unsafe: bool) -> Result<Isolation> { |
| 214 | let problems = findings.problems(); |
| 215 | if problems.is_empty() { |
| 216 | return Ok(Isolation::NestedContainer); |
| 217 | } |
| 218 | if allow_unsafe { |
| 219 | return Ok(Isolation::Container); |
| 220 | } |
| 221 | bail!( |
| 222 | "this runner is not sandboxed from its machine, so it will not take work:\n - {}\nRun it as `docker run --runtime=sysbox-runc …` without the machine's Docker socket (see https://docs.g1t.sh/guides/self-hosted-runners/#run-it-as-a-container), or set {UNSAFE_ENV}=1 to run anyway with jobs able to reach the machine.", |
| 223 | problems.join("\n - ") |
| 224 | ) |
| 225 | } |
| 226 | |
| 227 | // --- Sandbox drivers -------------------------------------------------------- |
| 228 | |
| 229 | /// What makes and destroys a job's sandbox. Each job gets a fresh one, |
| 230 | /// which never touches the machine, and which is gone after the job. |
| 231 | /// Making and destroying a Docker sandbox is exec.rs; a driver for another |
| 232 | /// kind adds its own when it is built. |
| 233 | pub trait SandboxDriver { |
| 234 | /// What it is called in the log. |
| 235 | fn name(&self) -> &'static str; |
| 236 | /// Whether this machine can make sandboxes with it: the isolation it |
| 237 | /// gives here, or why it cannot. |
| 238 | fn preflight(&self) -> Result<Isolation>; |
| 239 | } |
| 240 | |
| 241 | /// Linux, built: the runner's own Docker Engine, inside the runner's |
| 242 | /// Sysbox container (engine.rs), and a container per job on it (exec.rs), |
| 243 | /// with no mounts from the machine. `allow_unsafe`: [`UNSAFE_ENV`] is set. |
| 244 | pub struct NestedDocker { |
| 245 | pub allow_unsafe: bool, |
| 246 | } |
| 247 | |
| 248 | impl SandboxDriver for NestedDocker { |
| 249 | fn name(&self) -> &'static str { |
| 250 | "Docker inside the runner's container" |
| 251 | } |
| 252 | fn preflight(&self) -> Result<Isolation> { |
| 253 | check_nested(&Findings::read(), self.allow_unsafe) |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | /// The same containers, on a Docker Engine shared with the machine: a |
| 258 | /// runner installed on the machine, or a container given its socket. |
| 259 | pub struct SharedDocker; |
| 260 | |
| 261 | impl SandboxDriver for SharedDocker { |
| 262 | fn name(&self) -> &'static str { |
| 263 | "the machine's Docker" |
| 264 | } |
| 265 | fn preflight(&self) -> Result<Isolation> { |
| 266 | Ok(Isolation::Container) |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | /// The driver for jobs native to this OS, once there is one: what a |
| 271 | /// Windows or macOS runner in host mode says is coming. |
| 272 | pub fn native_driver() -> Option<Box<dyn SandboxDriver>> { |
| 273 | match std::env::consts::OS { |
| 274 | "windows" => Some(Box::new(HyperVContainer)), |
| 275 | "macos" => Some(Box::new(MacVm)), |
| 276 | _ => None, |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | /// **Windows: not built.** The design: |
| 281 | /// |
| 282 | /// - A minimal launcher on the host, the only part that runs there. It |
| 283 | /// polls nothing and runs no job code: it creates a sandbox from a |
| 284 | /// signed image when the runner inside asks for one, and destroys it |
| 285 | /// when the job ends. Its whole job is "create from signed image" and |
| 286 | /// "destroy", so there is little to attack. |
| 287 | /// - Each job is a Windows container with Hyper-V isolation |
| 288 | /// (`docker run --isolation=hyperv`, or the HCS API directly): its own |
| 289 | /// utility VM and kernel, so a job that escapes its container is still |
| 290 | /// in a VM, not on the host. Process isolation shares the host's kernel |
| 291 | /// and is not enough. |
| 292 | /// - The image is pinned by digest and its signature checked before it is |
| 293 | /// used; the job gets the harness read-only, its own token, and a |
| 294 | /// scratch disk destroyed with the VM. |
| 295 | /// |
| 296 | /// It needs a Windows host with Hyper-V and the Containers feature, and |
| 297 | /// cannot be built or tested on Linux. Until it is, Windows jobs run in |
| 298 | /// host mode, which is deprecated. |
| 299 | pub struct HyperVContainer; |
| 300 | |
| 301 | impl SandboxDriver for HyperVContainer { |
| 302 | fn name(&self) -> &'static str { |
| 303 | "Windows containers with Hyper-V isolation" |
| 304 | } |
| 305 | fn preflight(&self) -> Result<Isolation> { |
| 306 | bail!("Hyper-V-isolated Windows containers are not built yet; Windows jobs run on the host (deprecated)") |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | /// **macOS: not built.** The design: |
| 311 | /// |
| 312 | /// - The same minimal host launcher as on Windows, using Apple's |
| 313 | /// Virtualization framework (`VZVirtualMachine`) to boot an ephemeral |
| 314 | /// macOS VM per job from a signed base image, cloned copy-on-write |
| 315 | /// (APFS clones) so a fresh VM costs seconds, and deleted after. |
| 316 | /// - Apple's licence allows at most two macOS VMs per Mac at once, so a |
| 317 | /// Mac runs two jobs at a time at most, and the launcher refuses a |
| 318 | /// third. |
| 319 | /// - The runner and harness run inside the VM; the host keeps only the |
| 320 | /// launcher and the signed images. |
| 321 | /// |
| 322 | /// It needs Apple silicon and macOS 13 or later, and cannot be built or |
| 323 | /// tested on Linux. Until it is, macOS jobs run in host mode, which is |
| 324 | /// deprecated. |
| 325 | pub struct MacVm; |
| 326 | |
| 327 | impl MacVm { |
| 328 | /// Apple's macOS licence: two virtual copies per Mac. |
| 329 | pub const MAX_VMS: u32 = 2; |
| 330 | } |
| 331 | |
| 332 | impl SandboxDriver for MacVm { |
| 333 | fn name(&self) -> &'static str { |
| 334 | "ephemeral macOS VMs" |
| 335 | } |
| 336 | fn preflight(&self) -> Result<Isolation> { |
| 337 | bail!("ephemeral macOS VMs (at most {} per Mac) are not built yet; macOS jobs run on the host (deprecated)", MacVm::MAX_VMS) |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | #[cfg(test)] |
| 342 | mod tests { |
| 343 | use super::*; |
| 344 | |
| 345 | const PLAIN: &str = "\ |
| 346 | 611 590 0:52 / / rw,relatime master:180 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/A,upperdir=/var/lib/docker/overlay2/x/diff |
| 347 | 612 611 0:55 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw |
| 348 | 620 611 254:1 /var/lib/docker/volumes/g1t-runner/_data /data rw,relatime - ext4 /dev/vda1 rw |
| 349 | 621 611 254:1 /var/lib/docker/containers/abc/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw |
| 350 | "; |
| 351 | |
| 352 | const WITH_SOCKET: &str = "\ |
| 353 | 611 590 0:52 / / rw,relatime master:180 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/A |
| 354 | 622 611 0:26 /docker.sock /run/docker.sock rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,size=3280376k,mode=755 |
| 355 | "; |
| 356 | |
| 357 | const RENAMED_SOCKET: &str = "\ |
| 358 | 623 611 0:26 /docker.sock /tmp/engine rw,nosuid,nodev - tmpfs tmpfs rw |
| 359 | 624 611 0:27 /containerd/containerd.sock /run/containerd/containerd.sock rw - tmpfs tmpfs rw |
| 360 | "; |
| 361 | |
| 362 | const SYSBOX: &str = "\ |
| 363 | 1000 990 0:60 / / rw,relatime - shiftfs /var/lib/sysbox/shiftfs/x rw |
| 364 | 1001 1000 0:61 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw |
| 365 | 1002 1001 0:62 /proc/uptime /proc/uptime rw,nosuid,nodev,relatime - fuse sysboxfs rw,user_id=0,group_id=0,default_permissions,allow_other |
| 366 | 1003 1000 254:1 /var/lib/sysbox/docker/abc /var/lib/docker rw,relatime - ext4 /dev/vda1 rw |
| 367 | "; |
| 368 | |
| 369 | #[test] |
| 370 | fn a_mounted_docker_socket_is_found() { |
| 371 | assert!(mounted_engine_sockets(PLAIN).is_empty()); |
| 372 | assert_eq!(mounted_engine_sockets(WITH_SOCKET), vec!["/run/docker.sock".to_owned()]); |
| 373 | // The same socket under another name, and containerd's. |
| 374 | assert_eq!(mounted_engine_sockets(RENAMED_SOCKET), vec!["/tmp/engine".to_owned(), "/run/containerd/containerd.sock".to_owned()]); |
| 375 | // Sysbox's own Docker data mount is not a socket. |
| 376 | assert!(mounted_engine_sockets(SYSBOX).is_empty()); |
| 377 | } |
| 378 | |
| 379 | #[test] |
| 380 | fn escaped_paths_are_read() { |
| 381 | assert_eq!(mounted_engine_sockets("1 0 0:1 /a\\040b/docker.sock /x\\040y rw - tmpfs tmpfs rw"), vec!["/x y".to_owned()]); |
| 382 | } |
| 383 | |
| 384 | #[test] |
| 385 | fn sysbox_and_a_user_namespace_are_told() { |
| 386 | assert!(under_sysbox(SYSBOX)); |
| 387 | assert!(!under_sysbox(PLAIN)); |
| 388 | assert!(root_is_the_machines(" 0 0 4294967295\n")); |
| 389 | assert!(!root_is_the_machines(" 0 165536 65536\n")); |
| 390 | } |
| 391 | |
| 392 | #[test] |
| 393 | fn a_nested_runner_refuses_the_machines_socket_unless_told_loudly() { |
| 394 | let sysbox = Findings::from_text(SYSBOX, "0 165536 65536"); |
| 395 | assert_eq!(check_nested(&sysbox, false).unwrap(), Isolation::NestedContainer); |
| 396 | |
| 397 | let socket = Findings::from_text(&format!("{SYSBOX}{WITH_SOCKET}"), "0 165536 65536"); |
| 398 | let refused = check_nested(&socket, false).unwrap_err().to_string(); |
| 399 | assert!(refused.contains("/run/docker.sock"), "{refused}"); |
| 400 | assert!(refused.contains(UNSAFE_ENV), "{refused}"); |
| 401 | assert_eq!(check_nested(&socket, true).unwrap(), Isolation::Container); |
| 402 | |
| 403 | // --privileged, without Sysbox: root here is the machine's. |
| 404 | let privileged = Findings::from_text(PLAIN, "0 0 4294967295"); |
| 405 | assert!(check_nested(&privileged, false).unwrap_err().to_string().contains("sysbox-runc")); |
| 406 | |
| 407 | let host = Findings { docker_host: Some("tcp://10.0.0.1:2375".into()), ..Findings::from_text(SYSBOX, "0 165536 65536") }; |
| 408 | assert!(check_nested(&host, false).is_err()); |
| 409 | } |
| 410 | |
| 411 | #[test] |
| 412 | fn host_mode_says_it_is_deprecated() { |
| 413 | assert!(HOST_MODE_AT_START.starts_with("Deprecated:")); |
| 414 | assert!(HOST_MODE_AT_START.contains("will be removed")); |
| 415 | assert!(HOST_MODE_IN_JOB.starts_with("This job ran on the host, unsandboxed")); |
| 416 | assert!(HOST_MODE_IN_JOB.contains("Host mode will be removed")); |
| 417 | assert!(host_mode_refuses("acme/web").contains("acme/web is public")); |
| 418 | assert_eq!(Isolation::NestedContainer.as_str(), "nested-container"); |
| 419 | assert_eq!(Isolation::Host.as_str(), "host"); |
| 420 | } |
| 421 | |
| 422 | #[test] |
| 423 | fn the_unbuilt_drivers_say_so() { |
| 424 | assert!(HyperVContainer.preflight().is_err()); |
| 425 | assert!(MacVm.preflight().unwrap_err().to_string().contains("at most 2")); |
| 426 | assert_eq!(SharedDocker.preflight().unwrap(), Isolation::Container); |
| 427 | assert_eq!(MacVm::MAX_VMS, 2); |
| 428 | } |
| 429 | } |