Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
179 linesCodeBlameRaw
1//! What `g1t-runner register` keeps: where g1t is, who this runner is, its
2//! credential, and how it runs work. One JSON file in the runner's folder,
3//! readable by its owner only.
4
5use std::path::{Path, PathBuf};
6
7use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
11pub struct Config {
12 /// The site, as given to `--url`: `https://g1t.sh`.
13 pub url: String,
14 /// The API the runner calls: `https://api.g1t.sh`.
15 pub api: String,
16 /// The runner's id and name.
17 pub runner: String,
18 pub name: String,
19 /// Its credential (`g1tr_…`). Rotated by g1t every day.
20 pub credential: String,
21 pub workspace: String,
22 #[serde(default)]
23 pub repo: Option<String>,
24 #[serde(default)]
25 pub group: Option<String>,
26 pub labels: Vec<String>,
27 /// Runs one job, then removes itself.
28 #[serde(default)]
29 pub ephemeral: bool,
30 /// Where jobs keep their files when they run on this machine directly.
31 pub work_dir: PathBuf,
32 /// Runs work in Docker containers (the default), or directly.
33 #[serde(default = "yes")]
34 pub docker: bool,
35 /// The image workflow jobs run in when they name no `container:`.
36 #[serde(default)]
37 pub image: Option<String>,
38 /// A Linux build of `g1t-runner` to run inside containers, instead of
39 /// this binary (on Linux) or the release's (elsewhere).
40 #[serde(default)]
41 pub harness: Option<PathBuf>,
42 /// Checks for new versions and updates itself.
43 #[serde(default = "yes")]
44 pub auto_update: bool,
45 /// Each job container's memory limit (`--memory`): `8g`. Three
46 /// quarters of the machine's (or the runner container's) memory if
47 /// left out.
48 #[serde(default)]
49 pub memory: Option<String>,
50 /// Each job container's CPUs (`--cpus`): `2`, `1.5`. Every CPU if left out.
51 #[serde(default)]
52 pub cpus: Option<String>,
53 /// The most processes in a job container (`--pids-limit`). 4096 if left out.
54 #[serde(default)]
55 pub pids_limit: Option<u32>,
56 /// With nested-container isolation, a job that uses Docker gets an
57 /// engine of its own. Off: jobs have no Docker.
58 #[serde(default = "yes")]
59 pub job_docker: bool,
60 /// The image a job's own engine runs: `docker:28-dind` if left out.
61 #[serde(default)]
62 pub job_docker_image: Option<String>,
63 /// Runs jobs native to this OS, each in a throwaway VM of its own
64 /// (`--native`, native.rs): Windows Sandbox on Windows.
65 #[serde(default)]
66 pub native: bool,
67}
68
69fn yes() -> bool {
70 true
71}
72
73/// The runner's folder: `--dir`, `G1T_RUNNER_DIR`, else `.g1t-runner` in
74/// the home folder.
75pub fn folder(given: Option<&str>) -> PathBuf {
76 if let Some(dir) = given.filter(|d| !d.is_empty()) {
77 return PathBuf::from(dir);
78 }
79 if let Some(dir) = std::env::var_os("G1T_RUNNER_DIR").filter(|d| !d.is_empty()) {
80 return PathBuf::from(dir);
81 }
82 let home = std::env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" })
83 .map(PathBuf::from)
84 .unwrap_or_else(|| PathBuf::from("."));
85 home.join(".g1t-runner")
86}
87
88pub fn path(folder: &Path) -> PathBuf {
89 folder.join("config.json")
90}
91
92pub fn load(folder: &Path) -> Result<Config> {
93 let file = path(folder);
94 let text = std::fs::read_to_string(&file)
95 .with_context(|| format!("this runner is not registered ({} not found): run `g1t-runner register` first", file.display()))?;
96 serde_json::from_str(&text).with_context(|| format!("{} does not read", file.display()))
97}
98
99/// Writes the configuration so only its owner can read it, in a folder
100/// only its owner can read (private.rs), through a temporary file, so a
101/// crash never leaves half a credential.
102pub fn save(folder: &Path, config: &Config) -> Result<()> {
103 super::private::folder(folder, "--dir")?;
104 super::private::write_private(&path(folder), serde_json::to_string_pretty(config)?.as_bytes())
105}
106
107pub fn forget(folder: &Path) {
108 let _ = std::fs::remove_file(path(folder));
109}
110
111/// The API for a site: `https://g1t.sh` calls `https://api.g1t.sh`.
112pub fn api_for(url: &str) -> String {
113 let url = url.trim_end_matches('/');
114 match url.split_once("://") {
115 Some((scheme, host)) if !host.starts_with("api.") && !host.starts_with("localhost") && !host.starts_with("127.") => {
116 format!("{scheme}://api.{host}")
117 }
118 _ => url.to_owned(),
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn the_api_is_found_from_the_site() {
128 assert_eq!(api_for("https://g1t.sh"), "https://api.g1t.sh");
129 assert_eq!(api_for("https://g1t.sh/"), "https://api.g1t.sh");
130 assert_eq!(api_for("https://api.g1t.sh"), "https://api.g1t.sh");
131 assert_eq!(api_for("https://git.example.com"), "https://api.git.example.com");
132 assert_eq!(api_for("http://localhost:8787"), "http://localhost:8787");
133 }
134
135 #[test]
136 fn an_older_configuration_still_reads() {
137 let old = r#"{"url":"https://g1t.sh","api":"https://api.g1t.sh","runner":"rnr_1","name":"a","credential":"g1tr_x",
138 "workspace":"acme","labels":["self-hosted"],"work_dir":"/tmp/w"}"#;
139 let config: Config = serde_json::from_str(old).unwrap();
140 assert!(config.docker && config.job_docker && config.auto_update);
141 assert_eq!(config.memory, None);
142 }
143
144 #[test]
145 fn a_configuration_survives_a_round_trip() {
146 let dir = std::env::temp_dir().join(format!("g1t-runner-config-{}", std::process::id()));
147 let config = Config {
148 url: "https://g1t.sh".into(),
149 api: "https://api.g1t.sh".into(),
150 runner: "rnr_1".into(),
151 name: "build-01".into(),
152 credential: "g1tr_x".into(),
153 workspace: "acme".into(),
154 repo: None,
155 group: Some("Default".into()),
156 labels: vec!["self-hosted".into(), "linux".into()],
157 ephemeral: false,
158 work_dir: dir.join("work"),
159 docker: true,
160 image: None,
161 harness: None,
162 auto_update: true,
163 memory: Some("4g".into()),
164 cpus: None,
165 pids_limit: None,
166 job_docker: true,
167 job_docker_image: None,
168 native: false,
169 };
170 save(&dir, &config).unwrap();
171 let back = load(&dir).unwrap();
172 assert_eq!(back.runner, "rnr_1");
173 assert_eq!(back.labels, config.labels);
174 assert_eq!(back.memory.as_deref(), Some("4g"));
175 forget(&dir);
176 assert!(load(&dir).is_err());
177 let _ = std::fs::remove_dir_all(&dir);
178 }
179}