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.
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};
47pub use control::{
48    Busy, Check, CheckStatus, ConfigCheck, ConfigFile, Doctor, PauseQuery, Paused, RunningJob,
49};
50pub use events::{Event, EventEnvelope, EventQuery};
51pub use jobs::{
52    Bucket, CacheHistory, CacheHistoryQuery, CacheKind, CacheSize, CacheVolume, Caches, HostSample,
53    HostSampleQuery, HostSamples, Job, JobMetrics, JobPage, JobQuery, JobRunner, JobState,
54    JobSummary, LogQuery, LogSummary, Sample, SampleSeries, Stats, StatsQuery, StatsRow, Step,
55    Timeline,
56};
57pub use records::{
58    Record, RecordLimits, RecordNote, RecordRemoval, Records, RemoveRecordsQuery, RemovedRecords,
59    SHELL_PROTOCOL, ShellQuery,
60};
61pub use sizing::{Advice, JobSizing, Peaks, ProfileSizing, Resize, Sizing, SizingQuery, Spread};
62
63/// Who sent a request, as the transport authenticated it. Handlers get it from one place, so a new transport only
64/// has to produce one.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "kebab-case", tag = "kind")]
67pub enum Principal {
68    /// The user the daemon runs as, on the local Unix socket.
69    LocalOwner {
70        /// The Unix user ID the socket's peer runs as.
71        uid: u32,
72    },
73}
74
75/// `GET /v1/status`: what the daemon serves and runs right now.
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct Status {
78    /// The daemon's version, from its `Cargo.toml`.
79    pub version: String,
80    /// Set once the daemon stops polling to shut down.
81    pub shutting_down: bool,
82    /// The memory budget and what runners use of it.
83    pub budget: Budget,
84    /// Every configured profile with its slots.
85    pub profiles: Vec<ProfileStatus>,
86    /// Every configured repository.
87    pub repositories: Vec<RepositoryStatus>,
88    /// Queued jobs the repositories' runners can take and none holds yet, at the last poll, oldest first. No event
89    /// carries the list: `job.queued` announces a job, `job.started` and `job.finished` its leaving the queue.
90    /// A repository whose last listing failed, as while GitHub rate-limits runnerd, or that is no longer polled
91    /// because runnerd is shutting down, shows no jobs here, while the history keeps its jobs queued.
92    #[serde(default)]
93    pub queued: Vec<QueuedJob>,
94}
95
96/// The memory budget runners and the mirror share, in bytes.
97#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
98pub struct Budget {
99    /// The systemd slice runner containers run in.
100    pub slice: String,
101    /// The slice's `MemoryMax`; `None` when it is unlimited.
102    pub memory_max: Option<u64>,
103    /// Memory the running containers reserve: the runners' caps, or under measured admission the larger of each
104    /// runner's predicted peak and its last sampled memory, and the caps of the seed containers, shells and the
105    /// mirror.
106    pub memory_in_use: u64,
107    /// What a new runner must fit in beside them.
108    #[serde(default)]
109    pub admission: AdmissionPolicy,
110    /// Under measured admission, whether the slice is at the high-water mark, so no runner starts.
111    #[serde(default)]
112    pub held: bool,
113    /// The mirror's part of `memory_in_use`: its cap while it is enabled or still running, else 0.
114    #[serde(default)]
115    pub mirror_memory: u64,
116    /// The seed containers' part of `memory_in_use`, one per profile that reseeds, by profile name.
117    #[serde(default)]
118    pub seed_memory: Vec<SeedMemory>,
119    /// The shells' part of `memory_in_use`, one per shell open in a record, by job.
120    #[serde(default)]
121    pub shell_memory: Vec<ShellMemory>,
122}
123
124impl Budget {
125    /// How runners are admitted, as clients add it to the budget's line: nothing under caps, else `by predicted
126    /// peaks`, and why nothing starts while the slice is at the high-water mark.
127    pub fn admission_note(&self) -> Option<&'static str> {
128        match (self.admission, self.held) {
129            (AdmissionPolicy::Caps, _) => None,
130            (AdmissionPolicy::Measured, false) => Some("by predicted peaks"),
131            (AdmissionPolicy::Measured, true) => Some(
132                "by predicted peaks; the slice is past 90% of its MemoryMax, so no runner starts",
133            ),
134        }
135    }
136}
137
138/// A shell container's memory cap: its record's profile's limit when the shell opened.
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct ShellMemory {
141    /// The job whose record the shell runs.
142    pub job: u64,
143    /// Its cap in bytes.
144    pub memory: u64,
145}
146
147/// A running seed container's memory cap: its profile's limit when the reseed started.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct SeedMemory {
150    /// The profile it reseeds.
151    pub profile: String,
152    /// Its cap in bytes.
153    pub memory: u64,
154}
155
156/// A runner profile and what runs in its slots.
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158pub struct ProfileStatus {
159    /// The name under `[profiles]` in `runnerd.toml`.
160    pub name: String,
161    /// The image its runners run, such as `runnerd-runner:base`.
162    pub image: String,
163    /// CPUs per slot.
164    pub cpus: u32,
165    /// Bytes per slot.
166    pub memory: u64,
167    /// Set while slot 1's layers become the profile's snapshot.
168    pub reseeding: bool,
169    /// Index 0 is slot 1.
170    pub slots: Vec<SlotStatus>,
171}
172
173/// One slot of a profile: room for one job.
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct SlotStatus {
176    /// 1-based.
177    pub slot: usize,
178    /// `None` when the slot is free.
179    pub runner: Option<RunnerStatus>,
180}
181
182/// A runner registered for one job, in its container.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct RunnerStatus {
185    /// `owner/name` the runner is registered on.
186    pub repository: String,
187    /// The runner's name, also its container's.
188    pub name: String,
189    /// The runner's ID on GitHub.
190    pub runner_id: u64,
191    /// The Docker container ID.
192    pub container: String,
193    /// Whether GitHub reports it running a job.
194    pub busy: bool,
195    /// Seconds since the Unix epoch.
196    pub started_at: u64,
197    /// What it takes of the budget: its cap under caps; under measured admission the larger of its predicted peak
198    /// and its last sampled memory. 0 from a daemon before measured admission.
199    #[serde(default)]
200    pub reserved: u64,
201}
202
203/// A configured repository and its queue.
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct RepositoryStatus {
206    /// `owner/name` on GitHub.
207    pub name: String,
208    /// The profile its jobs run in.
209    pub profile: String,
210    /// The labels its runners register with.
211    pub labels: Vec<String>,
212    /// Paused repositories get no new runners; their running jobs finish.
213    pub paused: bool,
214    /// Queued jobs whose labels these runners match, at the last poll.
215    pub queued_jobs: usize,
216}
217
218/// A queued job runners of this daemon can take, as GitHub listed it at the last poll.
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220pub struct QueuedJob {
221    /// GitHub's job ID.
222    pub id: u64,
223    /// The repository whose runners can take it, `owner/name`.
224    pub repository: String,
225    /// The workflow run it belongs to.
226    pub run_id: u64,
227    /// 1 for the first attempt of the run, higher for re-runs.
228    pub run_attempt: u32,
229    /// The workflow's name.
230    pub workflow: Option<String>,
231    /// The job's name in the workflow.
232    pub name: String,
233    /// The branch the run is for.
234    pub branch: Option<String>,
235    /// The job's page on GitHub.
236    pub url: Option<String>,
237    /// When GitHub queued it, in seconds since the Unix epoch; 0 if GitHub did not say.
238    pub queued_at: u64,
239}
240
241/// `POST /v1/shutdown`.
242#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
243pub struct ShutdownRequest {
244    /// Kill running jobs and delete their runners instead of waiting for them.
245    #[serde(default)]
246    pub force: bool,
247}
248
249/// The body of every error response.
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251pub struct ErrorResponse {
252    /// What went wrong, for people to read.
253    pub message: String,
254}