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 | ||
| 14 | use std::process::{Child, Stdio}; | |
| 15 | ||
| 16 | use super::backend::quiet; | |
| 17 | ||
| 18 | /// The runner's image, published with each release. | |
| 19 | pub const IMAGE: &str = "g1t.sh/flagon-io/g1t-runner:latest"; | |
| 20 | /// The container the app runs, and the volume its registration is kept on. | |
| 21 | pub const NAME: &str = "g1t-desktop-runner"; | |
| 22 | pub const VOLUME: &str = "g1t-desktop-runner"; | |
| 23 | ||
| 24 | fn 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 | ||
| 28 | fn 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. | |
| 37 | pub 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. | |
| 53 | pub 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. | |
| 60 | pub 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. | |
| 65 | pub 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. | |
| 72 | pub 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. | |
| 78 | pub 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. | |
| 99 | pub 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. | |
| 104 | pub 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. | |
| 114 | pub 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)] | |
| 126 | mod 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.