runnerd_core/api/control.rs
1//! The control endpoints' types: pausing with or without cancelling running jobs, what a refused request names,
2//! the config file and its check, and the doctor's checks. Plain data; the engine and the daemon act on them.
3
4use std::fmt;
5
6use serde::{Deserialize, Serialize};
7
8use crate::api::AdmissionPolicy;
9
10/// A runner of this daemon that holds a slot, and the job it runs, if it took one.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct RunningJob {
13 /// `owner/name` the runner is registered on.
14 pub repository: String,
15 /// The runner's name, also its container's.
16 pub runner: String,
17 /// The job's ID; `None` while the runner waits for a job or before its log shows which it took.
18 pub job: Option<u64>,
19}
20
21impl fmt::Display for RunningJob {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 match self.job {
24 Some(job) => write!(f, "job {job} on {}", self.runner),
25 None => write!(f, "{}", self.runner),
26 }
27 }
28}
29
30/// The body of a 409 refused because runners use what the request would change. Has the `message` of an
31/// [`ErrorResponse`](super::ErrorResponse), so a client that reads only that still gets it.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct Busy {
34 /// What went wrong, naming the jobs.
35 pub message: String,
36 /// The runners in the way.
37 pub jobs: Vec<RunningJob>,
38}
39
40/// `POST /v1/repositories/{owner}/{name}/pause` and `POST /v1/repositories/pause` query parameters.
41#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
42pub struct PauseQuery {
43 /// Cancel the repository's running jobs and remove their runners. Without it, a pause while the repository
44 /// runs a job is refused with [`Busy`].
45 #[serde(default)]
46 pub force: bool,
47}
48
49/// `POST /v1/repositories/{owner}/{name}/pause` and `POST /v1/repositories/pause`: the repository, or every one,
50/// is paused, and stays paused when the daemon restarts.
51#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
52pub struct Paused {
53 /// The runners a forced pause stopped: those whose runs it cancelled go once they report the job cancelled,
54 /// within 90 s, the others at once. Empty without `force`.
55 pub stopped: Vec<RunningJob>,
56}
57
58/// `GET /v1/config`: the config the daemon runs with. Parse `text` with `config::Config::parse` for its fields.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct ConfigFile {
61 /// The file it was read from, and `PUT /v1/config` writes.
62 pub path: String,
63 /// The TOML, as last loaded or written by the daemon, with each `[[notify.targets]]` entry's `url` and header
64 /// values shown as `config::SECRET_SHOWN`; `PUT /v1/config` puts them back.
65 pub text: String,
66 /// `config::revision` of `text`: send it as `PUT /v1/config?revision=` and a config replaced meanwhile, as
67 /// by auto sizing, is not overwritten (409).
68 pub revision: String,
69}
70
71/// `PUT /v1/config` query parameters.
72#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
73pub struct ReplaceQuery {
74 /// The revision of the config the new text was made from; absent, the text replaces whatever runs.
75 pub revision: Option<String>,
76}
77
78/// `POST /v1/config/check` and `PUT /v1/config`: what `runnerd check-config` prints for a config. `PUT` refuses
79/// an invalid one with 422 and this body, which has an [`ErrorResponse`](super::ErrorResponse)'s `message`.
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct ConfigCheck {
82 /// Whether the daemon can run it.
83 pub valid: bool,
84 /// `valid, memory caps sum to …`, or why it is not valid.
85 pub message: String,
86 /// What a valid config should still fix.
87 pub warnings: Vec<String>,
88 /// `[host] admission`, which decides what must fit `MemoryMax`: `memory_caps` under caps, `memory_needed`
89 /// under measured.
90 pub admission: AdmissionPolicy,
91 /// Bytes every slot and the mirror take at their caps, all at once, when it parses.
92 pub memory_caps: Option<u64>,
93 /// Bytes the admission rule must fit in `MemoryMax`, when it parses: `memory_caps` under caps; under measured,
94 /// the largest single runner with the mirror.
95 pub memory_needed: Option<u64>,
96 /// The `MemoryMax` of the slice it names; `None` when unlimited or unknown.
97 pub memory_max: Option<u64>,
98 /// From `PUT /v1/config`, once written: the revision of the text written, to make the next change against.
99 /// `None` from a check.
100 pub revision: Option<String>,
101}
102
103/// `GET /v1/doctor`: what `runnerd doctor` checks, for the config the daemon runs with.
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct Doctor {
106 /// In the order a new user fixes them.
107 pub checks: Vec<Check>,
108}
109
110/// One thing the host needs to run runners. Its `Display` is the line `runnerd doctor` prints.
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub struct Check {
113 /// Such as `Docker` or `sysbox`.
114 pub name: String,
115 /// The outcome.
116 pub status: CheckStatus,
117 /// What was found.
118 pub detail: String,
119 /// What to run or change to pass, for a failed check.
120 pub fix: Option<String>,
121}
122
123/// How a [`Check`] came out.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(rename_all = "kebab-case")]
126pub enum CheckStatus {
127 /// Ready.
128 Pass,
129 /// Runners cannot run until it is fixed.
130 Fail,
131 /// Works, but unsafe.
132 Warn,
133 /// Not checked, because a check it depends on failed or no config was given.
134 Skip,
135 /// For information.
136 Info,
137}
138
139impl CheckStatus {
140 /// Its label, as every client shows it: `ok`, `FAIL`, `WARN`, `skip` or `info`; a failure and a warning in
141 /// capitals so they stand out uncoloured.
142 pub fn label(self) -> &'static str {
143 match self {
144 CheckStatus::Pass => "ok",
145 CheckStatus::Fail => "FAIL",
146 CheckStatus::Warn => "WARN",
147 CheckStatus::Skip => "skip",
148 CheckStatus::Info => "info",
149 }
150 }
151
152 /// What it means for the reader: a warning is not a failure.
153 pub fn tone(self) -> super::Tone {
154 match self {
155 CheckStatus::Pass => super::Tone::Good,
156 CheckStatus::Fail => super::Tone::Bad,
157 CheckStatus::Warn => super::Tone::Warn,
158 CheckStatus::Skip => super::Tone::Quiet,
159 CheckStatus::Info => super::Tone::Neutral,
160 }
161 }
162}
163
164impl Doctor {
165 /// Whether runners can run: no check failed. Warnings, skips and notes pass.
166 pub fn passed(&self) -> bool {
167 Doctor::all_passed(&self.checks)
168 }
169
170 /// `Doctor::passed` of checks not yet in a `Doctor`, as `runnerd doctor` runs them.
171 pub fn all_passed(checks: &[Check]) -> bool {
172 !checks.iter().any(|check| check.status == CheckStatus::Fail)
173 }
174}
175
176impl fmt::Display for Check {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 let status = self.status.label();
179 write!(f, "{status:<5} {}: {}", self.name, self.detail)?;
180 if let Some(fix) = &self.fix {
181 for (i, line) in fix.lines().enumerate() {
182 let label = if i == 0 { "fix:" } else { "" };
183 write!(f, "\n {label:<4} {line}")?;
184 }
185 }
186 Ok(())
187 }
188}