Skip to content
139 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.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}

This file's history is long; its oldest lines are credited to the oldest commit read.