Skip to main content

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