Skip to content
139 linesCodeBlameRaw
1//! The Sandboxed backend on Linux today: the runner's image under Sysbox,
2//! through this computer's Docker, exactly as docs.g1t.sh/guides/
3//! self-hosted-runners/#run-it-as-a-container runs it. Sysbox gives the
4//! runner's container a user namespace of its own and a Docker Engine
5//! inside it, so each job is a container inside the runner's container and
6//! neither reaches this computer (`nested-container` isolation). The
7//! machine's Docker socket is never given to it.
8//!
9//! The registration and credential are kept on a volume of their own, so
10//! the container can be replaced, when a newer image is out, without
11//! registering again. Keeping it up to date is pulling the image and,
12//! while the runner is idle, putting a new container in the old one's place.
13
14use std::process::{Child, Stdio};
15
16use super::backend::quiet;
17
18/// The runner's image, published with each release.
19pub const IMAGE: &str = "g1t.sh/flagon-io/g1t-runner:latest";
20/// The container the app runs, and the volume its registration is kept on.
21pub const NAME: &str = "g1t-desktop-runner";
22pub const VOLUME: &str = "g1t-desktop-runner";
23
24fn docker_ok(args: &[&str]) -> bool {
25 quiet("docker").args(args).stdout(Stdio::null()).stderr(Stdio::null()).status().is_ok_and(|status| status.success())
26}
27
28fn docker_text(args: &[&str]) -> Option<String> {
29 let out = quiet("docker").args(args).stderr(Stdio::null()).output().ok()?;
30 out.status.success().then(|| String::from_utf8_lossy(&out.stdout).trim().to_owned()).filter(|text| !text.is_empty())
31}
32
33/// The arguments of the `docker run` that starts the runner. Each
34/// variable's value comes from the environment of the `docker` command,
35/// never its command line, so a registration token is not in the
36/// computer's list of processes.
37pub fn run_args(names: &[&str]) -> Vec<String> {
38 let mut args: Vec<String> = ["run", "-d", "--name", NAME, "--runtime=sysbox-runc", "--label", "sh.g1t.desktop=runner", "-v"]
39 .iter()
40 .map(|s| s.to_string())
41 .collect();
42 args.push(format!("{VOLUME}:/data"));
43 for name in names {
44 args.push("-e".into());
45 args.push((*name).to_owned());
46 }
47 args.push(IMAGE.into());
48 args.push("register-and-run".into());
49 args
50}
51
52/// Fetches the newest image; whether the one here changed.
53pub fn pull() -> bool {
54 let before = docker_text(&["image", "inspect", "--format", "{{.Id}}", IMAGE]);
55 let pulled = docker_ok(&["pull", "--quiet", IMAGE]);
56 pulled && docker_text(&["image", "inspect", "--format", "{{.Id}}", IMAGE]) != before
57}
58
59/// Whether the image is here at all.
60pub fn have_image() -> bool {
61 docker_ok(&["image", "inspect", IMAGE])
62}
63
64/// Whether the running container is from an older image than the one here.
65pub fn outdated() -> bool {
66 let image = docker_text(&["image", "inspect", "--format", "{{.Id}}", IMAGE]);
67 let running = docker_text(&["inspect", "--format", "{{.Image}}", NAME]);
68 matches!((image, running), (Some(image), Some(running)) if image != running)
69}
70
71/// Whether a registration is kept here.
72pub fn registered() -> bool {
73 docker_ok(&["volume", "inspect", VOLUME])
74}
75
76/// Starts the runner's container in place of any earlier one, then
77/// follows its log: the child's output is the runner's.
78pub fn start(envs: &[(&str, String)]) -> Result<Child, String> {
79 let _ = docker_ok(&["rm", "-f", NAME]);
80 let names: Vec<&str> = envs.iter().map(|(name, _)| *name).collect();
81 let out = quiet("docker")
82 .args(run_args(&names))
83 .envs(envs.iter().map(|(name, value)| (*name, value.as_str())))
84 .stdout(Stdio::null())
85 .output()
86 .map_err(|e| format!("could not run docker: {e}"))?;
87 if !out.status.success() {
88 return Err(String::from_utf8_lossy(&out.stderr).trim().to_owned());
89 }
90 quiet("docker")
91 .args(["logs", "--follow", NAME])
92 .stdout(Stdio::piped())
93 .stderr(Stdio::piped())
94 .spawn()
95 .map_err(|e| format!("could not follow the runner's log: {e}"))
96}
97
98/// How the container's runner ended, while the container is still there.
99pub fn exit_code() -> Option<i32> {
100 docker_text(&["inspect", "--format", "{{.State.ExitCode}}", NAME]).and_then(|code| code.parse().ok())
101}
102
103/// Stops the container and removes it; its volume stays.
104pub fn stop(logs: Option<&mut Child>) {
105 let _ = docker_ok(&["stop", "--time", "20", NAME]);
106 let _ = docker_ok(&["rm", "-f", NAME]);
107 if let Some(logs) = logs {
108 let _ = logs.kill();
109 let _ = logs.wait();
110 }
111}
112
113/// Unregisters the runner kept on the volume, then removes the volume.
114pub fn forget() {
115 if registered() {
116 let mut args = vec!["run", "--rm", "--runtime=sysbox-runc", "-v"];
117 let mount = format!("{VOLUME}:/data");
118 args.push(&mount);
119 args.extend([IMAGE, "remove"]);
120 let _ = docker_ok(&args);
121 let _ = docker_ok(&["volume", "rm", "-f", VOLUME]);
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn the_runner_runs_under_sysbox_without_the_machines_docker() {
131 let args = run_args(&["G1T_RUNNER_URL", "G1T_RUNNER_TOKEN"]);
132 assert!(args.contains(&"--runtime=sysbox-runc".to_owned()));
133 assert!(args.contains(&"g1t-desktop-runner:/data".to_owned()));
134 assert!(!args.iter().any(|a| a.contains("docker.sock") || a == "--privileged"), "{args:?}");
135 // Values never on the command line: only the names.
136 assert!(args.windows(2).any(|w| w[0] == "-e" && w[1] == "G1T_RUNNER_TOKEN"));
137 assert_eq!(&args[args.len() - 2..], [IMAGE, "register-and-run"]);
138 }
139}