runnerd_core/api.rs
1//! The daemon's JSON API, version 1: the request and response types every client deserializes, and the
2//! principal a transport authenticates. Plain data only: no I/O, no Docker or GitHub types, no secrets.
3//!
4//! Routes, all under the authenticated principal:
5//!
6//! | Route | Body |
7//! |---|---|
8//! | `GET /v1/status` | [`Status`] |
9//! | `POST /v1/repositories/{owner}/{name}/pause?`[`PauseQuery`] | [`Paused`]; 409 [`Busy`] while it runs a job |
10//! | `POST /v1/repositories/{owner}/{name}/resume` | none, 204 |
11//! | `POST /v1/repositories/pause?`[`PauseQuery`] | [`Paused`]; 409 [`Busy`] while any runs a job, pausing none |
12//! | `POST /v1/repositories/resume` | none, 204: every repository resumed |
13//! | `POST /v1/profiles/{name}/reseed` | none, 202; 409 while it reseeds; `cache.reseeded` when done |
14//! | `POST /v1/caches/{name}/clean` | none, 204; 409 [`Busy`] while a runner uses it |
15//! | `GET /v1/config` | [`ConfigFile`] |
16//! | `POST /v1/config/check` | TOML in, [`ConfigCheck`] |
17//! | `PUT /v1/config` | TOML in, [`ConfigCheck`]; 422 [`ConfigCheck`] when invalid |
18//! | `GET /v1/doctor` | [`Doctor`] |
19//! | `POST /v1/shutdown` | [`ShutdownRequest`] in, 202 |
20//! | `GET /v1/jobs?`[`JobQuery`] | [`JobPage`] |
21//! | `GET /v1/jobs/{id}` | [`Job`] |
22//! | `GET /v1/jobs/{id}/samples` | [`SampleSeries`] |
23//! | `GET /v1/jobs/{id}/logs?`[`LogQuery`] | `text/plain`, one line per log line |
24//! | `GET /v1/host/samples?`[`HostSampleQuery`] | [`HostSamples`] |
25//! | `GET /v1/stats?`[`StatsQuery`] | [`Stats`] |
26//! | `GET /v1/sizing?`[`SizingQuery`] | [`Sizing`] |
27//! | `GET /v1/caches` | [`Caches`] |
28//! | `GET /v1/caches/history?`[`CacheHistoryQuery`] | [`CacheHistory`] |
29//! | `GET /v1/events?`[`EventQuery`] | `text/event-stream` of [`EventEnvelope`] |
30//! | `GET /v1/records` | [`Records`] |
31//! | `DELETE /v1/records/{job}` | [`RemovedRecords`]; 409 while a shell is open in it |
32//! | `DELETE /v1/records?`[`RemoveRecordsQuery`] | [`RemovedRecords`]; records with an open shell stay |
33//! | `POST /v1/records/{job}/shell?`[`ShellQuery`] | `Upgrade:` [`SHELL_PROTOCOL`], 101, then the shell's terminal; 409 when the budget is full |
34//!
35//! Errors carry an [`ErrorResponse`].
36
37use serde::{Deserialize, Serialize};
38
39mod admission;
40mod control;
41mod events;
42mod jobs;
43mod records;
44mod sizing;
45
46pub use admission::{Admission, AdmissionPolicy, Basis, CpuAdmission, JobPeak, cores, cpus};
47pub use control::{
48 Busy, Check, CheckStatus, ConfigCheck, ConfigFile, Doctor, PauseQuery, Paused, ReplaceQuery,
49 RunningJob,
50};
51pub use events::{Event, EventEnvelope, EventQuery};
52pub use jobs::{
53 Bucket, CacheHistory, CacheHistoryQuery, CacheKind, CacheSize, CacheVolume, Caches, HostSample,
54 HostSampleQuery, HostSamples, Job, JobMetrics, JobPage, JobQuery, JobRunner, JobState,
55 JobSummary, LogQuery, LogSummary, Sample, SampleSeries, Stats, StatsQuery, StatsRow, Step,
56 Timeline,
57};
58pub use records::{
59 Record, RecordLimits, RecordNote, RecordRemoval, Records, RemoveRecordsQuery, RemovedRecords,
60 SHELL_PROTOCOL, ShellQuery,
61};
62pub use sizing::{
63 Advice, JobSizing, Peaks, ProfileSizing, Resize, Sizing, SizingChange, SizingQuery, Spread,
64};
65
66/// Who sent a request, as the transport authenticated it. Handlers get it from one place, so a new transport only
67/// has to produce one.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "kebab-case", tag = "kind")]
70pub enum Principal {
71 /// The user the daemon runs as, on the local Unix socket.
72 LocalOwner {
73 /// The Unix user ID the socket's peer runs as.
74 uid: u32,
75 },
76}
77
78/// `GET /v1/status`: what the daemon serves and runs right now.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct Status {
81 /// The daemon's version, from its `Cargo.toml`.
82 pub version: String,
83 /// Set once the daemon stops polling to shut down.
84 pub shutting_down: bool,
85 /// The memory budget and what runners use of it.
86 pub budget: Budget,
87 /// Every configured profile with its slots.
88 pub profiles: Vec<ProfileStatus>,
89 /// Every configured repository.
90 pub repositories: Vec<RepositoryStatus>,
91 /// Queued jobs the repositories' runners can take and none holds yet, at the last poll, oldest first. No event
92 /// carries the list: `job.queued` announces a job, `job.started` and `job.finished` its leaving the queue.
93 /// A repository whose last listing failed, as while GitHub rate-limits runnerd, or that is no longer polled
94 /// because runnerd is shutting down, shows no jobs here, while the history keeps its jobs queued.
95 #[serde(default)]
96 pub queued: Vec<QueuedJob>,
97}
98
99/// The memory budget runners and the mirror share, in bytes.
100#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
101pub struct Budget {
102 /// The systemd slice runner containers run in.
103 pub slice: String,
104 /// The slice's `MemoryMax`; `None` when it is unlimited.
105 pub memory_max: Option<u64>,
106 /// Memory the running containers reserve: the runners' caps, or under measured admission the larger of each
107 /// runner's predicted peak and its last sampled memory, and the caps of the seed containers, shells and the
108 /// mirror.
109 pub memory_in_use: u64,
110 /// What a new runner must fit in beside them.
111 #[serde(default)]
112 pub admission: AdmissionPolicy,
113 /// Under measured admission, whether the slice is at the high-water mark, so no runner starts.
114 #[serde(default)]
115 pub held: bool,
116 /// The mirror's part of `memory_in_use`: its cap while it is enabled or still running, else 0.
117 #[serde(default)]
118 pub mirror_memory: u64,
119 /// The seed containers' part of `memory_in_use`, one per profile that reseeds, by profile name.
120 #[serde(default)]
121 pub seed_memory: Vec<SeedMemory>,
122 /// The shells' part of `memory_in_use`, one per shell open in a record, by job.
123 #[serde(default)]
124 pub shell_memory: Vec<ShellMemory>,
125 /// The CPU budget, under `[host] cpu_overcommit`. Absent without one, and from a daemon before CPU admission.
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub cpu: Option<CpuBudget>,
128}
129
130/// The CPU budget runners share, in millicores.
131#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
132pub struct CpuBudget {
133 /// The CPUs the slice may use: its `AllowedCPUs` or the host's, less under a lower `CPUQuota`.
134 pub cpus: u64,
135 /// Those times `cpu_overcommit`: what the runners' reservations must fit.
136 pub budget: u64,
137 /// What the runners reserve: each its predicted average CPU under measured admission, else its `cpus`.
138 pub in_use: u64,
139 /// Whether a queued job waits for CPU that memory and a slot have room for.
140 pub held: bool,
141}
142
143impl CpuBudget {
144 /// The budget's line as clients show it beside memory, such as `CPU 4.5 of 32 CPUs reserved` or, while a job
145 /// waits for it, `CPU 30 of 32 CPUs reserved; a job waits for CPU`.
146 pub fn line(&self) -> String {
147 let mut line = format!(
148 "CPU {} of {} CPUs reserved",
149 cores(self.in_use),
150 cores(self.budget)
151 );
152 if self.held {
153 line.push_str("; a job waits for CPU");
154 }
155 line
156 }
157}
158
159impl Budget {
160 /// How runners are admitted, as clients add it to the budget's line: nothing under caps, else `by predicted
161 /// peaks`, and why nothing starts while the slice is at the high-water mark.
162 pub fn admission_note(&self) -> Option<&'static str> {
163 match (self.admission, self.held) {
164 (AdmissionPolicy::Caps, _) => None,
165 (AdmissionPolicy::Measured, false) => Some("by predicted peaks"),
166 (AdmissionPolicy::Measured, true) => Some(
167 "by predicted peaks; the slice is past 90% of its MemoryMax, so no runner starts",
168 ),
169 }
170 }
171}
172
173/// A shell container's memory cap: its record's profile's limit when the shell opened.
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct ShellMemory {
176 /// The job whose record the shell runs.
177 pub job: u64,
178 /// Its cap in bytes.
179 pub memory: u64,
180}
181
182/// A running seed container's memory cap: its profile's limit when the reseed started.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct SeedMemory {
185 /// The profile it reseeds.
186 pub profile: String,
187 /// Its cap in bytes.
188 pub memory: u64,
189}
190
191/// A runner profile and what runs in its slots.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub struct ProfileStatus {
194 /// The name under `[profiles]` in `runnerd.toml`.
195 pub name: String,
196 /// The image its runners run, such as `runnerd-runner:base`.
197 pub image: String,
198 /// CPUs per slot.
199 pub cpus: u32,
200 /// Bytes per slot.
201 pub memory: u64,
202 /// Set while slot 1's layers become the profile's snapshot.
203 pub reseeding: bool,
204 /// Index 0 is slot 1.
205 pub slots: Vec<SlotStatus>,
206}
207
208/// One slot of a profile: room for one job.
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210pub struct SlotStatus {
211 /// 1-based.
212 pub slot: usize,
213 /// `None` when the slot is free.
214 pub runner: Option<RunnerStatus>,
215}
216
217/// A runner registered for one job, in its container.
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219pub struct RunnerStatus {
220 /// `owner/name` the runner is registered on.
221 pub repository: String,
222 /// The runner's name, also its container's.
223 pub name: String,
224 /// The runner's ID on GitHub.
225 pub runner_id: u64,
226 /// The Docker container ID.
227 pub container: String,
228 /// Whether GitHub reports it running a job.
229 pub busy: bool,
230 /// Seconds since the Unix epoch.
231 pub started_at: u64,
232 /// What it takes of the budget: its cap under caps; under measured admission the larger of its predicted peak
233 /// and its last sampled memory. 0 from a daemon before measured admission.
234 #[serde(default)]
235 pub reserved: u64,
236}
237
238/// A configured repository and its queue.
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240pub struct RepositoryStatus {
241 /// `owner/name` on GitHub.
242 pub name: String,
243 /// The profile its jobs run in.
244 pub profile: String,
245 /// The labels its runners register with.
246 pub labels: Vec<String>,
247 /// Paused repositories get no new runners; their running jobs finish.
248 pub paused: bool,
249 /// Queued jobs whose labels these runners match, at the last poll.
250 pub queued_jobs: usize,
251}
252
253/// A queued job runners of this daemon can take, as GitHub listed it at the last poll.
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255pub struct QueuedJob {
256 /// GitHub's job ID.
257 pub id: u64,
258 /// The repository whose runners can take it, `owner/name`.
259 pub repository: String,
260 /// The workflow run it belongs to.
261 pub run_id: u64,
262 /// 1 for the first attempt of the run, higher for re-runs.
263 pub run_attempt: u32,
264 /// The workflow's name.
265 pub workflow: Option<String>,
266 /// The job's name in the workflow.
267 pub name: String,
268 /// The branch the run is for.
269 pub branch: Option<String>,
270 /// The job's page on GitHub.
271 pub url: Option<String>,
272 /// When GitHub queued it, in seconds since the Unix epoch; 0 if GitHub did not say.
273 pub queued_at: u64,
274}
275
276/// `POST /v1/shutdown`.
277#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
278pub struct ShutdownRequest {
279 /// Kill running jobs and delete their runners instead of waiting for them.
280 #[serde(default)]
281 pub force: bool,
282}
283
284/// The body of every error response.
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286pub struct ErrorResponse {
287 /// What went wrong, for people to read.
288 pub message: String,
289}