//! The runner API between OxygenHub and `flow runner` (docs/m0.md §5). //! Both sides use these types, so they cannot drift. use crate::pipeline::{Job, Sandbox}; use crate::receipt::Receipt; use serde::{Deserialize, Serialize}; /// `POST /api/v1/flow/claim`. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ClaimRequest { /// The runner's name, shown on job pages. pub runner: String, /// Tiers this runner can run (`microvm`, `syscall`). pub runtimes: Vec, /// Memory the runner has free for another job, in MiB: the hub hands /// out only a job whose sandbox fits. Absent: no limit. #[serde(default, skip_serializing_if = "Option::is_none")] pub free_memory_mib: Option, /// vCPUs the runner has free for another job. Absent: no limit. #[serde(default, skip_serializing_if = "Option::is_none")] pub free_cpus: Option, /// What the runner is, for the hub's Runners page. #[serde(default, skip_serializing_if = "Option::is_none")] pub info: Option, /// The rootfs images the runner holds (BLAKE3 hex, as pipelines pin /// them): the hub offers only jobs whose image is among them /// (oxygenhub#73). Absent: the runner does not say, and any job may be /// offered. #[serde(default, skip_serializing_if = "Option::is_none")] pub images: Option>, /// The architecture of each image it holds, by hash, as the image's /// manifest records it (spec §11.2): the hub offers a job only to a /// runner whose architecture its image is for. #[serde(default, skip_serializing_if = "Option::is_none")] pub image_archs: Option>, /// The jobs this runner holds now: running, or finished with a result /// not yet reported. The hub ends any other job it has as running on /// this runner, which the runner lost by restarting or never received /// (oxygenhub#71). Absent: the runner does not say. #[serde(default, skip_serializing_if = "Option::is_none")] pub holding: Option>, } /// A runner's description: shown on the hub's Runners page, and what /// scheduling by label and architecture will match. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct RunnerInfo { /// `x86_64`, `aarch64`, `riscv64`, … pub arch: String, pub os: String, /// The `flow` version. pub version: String, /// From flow.toml `[runner] labels`. #[serde(default)] pub labels: Vec, /// Jobs at once, and the memory (MiB) and vCPUs they share. pub max_jobs: u32, #[serde(default)] pub memory_mib: Option, #[serde(default)] pub cpus: Option, } impl RunnerInfo { /// This machine and build, with the given labels and capacity. pub fn here(labels: Vec, max_jobs: u32, memory_mib: Option, cpus: Option) -> RunnerInfo { RunnerInfo { arch: std::env::consts::ARCH.into(), os: std::env::consts::OS.into(), version: env!("CARGO_PKG_VERSION").into(), labels, max_jobs, memory_mib, cpus, } } } /// `POST /api/v1/flow/runners/register`: exchange a one-time registration /// token from the hub for a runner credential, binding the runner's /// signing identity to it. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Register { pub token: String, pub name: String, /// The runner's Oxygen identity, hex of its encoding: its key signs /// every receipt, and the hub accepts no other for this runner. pub identity: String, pub info: RunnerInfo, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Registered { pub id: i64, pub name: String, /// The runner's bearer token from now on; shown once. pub credential: String, } /// A pinned subrepo to put in the workspace: `repo`'s state `heads`, at `path`. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SubrepoInput { pub path: String, /// `owner/name` on the hub. pub repo: String, pub clone_url: String, pub heads: Vec, } /// A secret for a job, as the hub supplies it (spec §9): written by the /// runner at `path` in the sandbox, 0400, on an in-memory mount; never in /// the environment, the receipt or an artifact. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SecretInput { pub name: String, pub path: String, pub value: String, } /// A claimed job: everything the runner needs, nothing more. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Claim { pub job_id: i64, pub run_id: i64, /// `owner/name`. pub repo: String, /// Where to fetch the repository from (`oxy` wire protocol). pub clone_url: String, /// Read access to this repository only, until the job finishes. pub token: String, /// The state to build, as full change hashes. pub heads: Vec, /// Hex BLAKE3 of the canonical pipeline. pub pipeline_hash: String, pub job: Job, pub sandbox: Sandbox, /// The hub's limit for this job. pub timeout_s: u64, /// Subrepos the state pins (Oxygen spec §7.8) that the job may read, /// fetched with the same token and written at their paths. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub subrepos: Vec, /// The state's hash on the hub (what checks are keyed by). #[serde(default, skip_serializing_if = "String::is_empty")] pub state: String, /// The bookmark the run is for. #[serde(default, skip_serializing_if = "String::is_empty")] pub bookmark: String, /// The run's number within its repository. #[serde(default)] pub run_number: i64, /// Variables the hub holds for the repository (its site's, its /// organisation's and its own, inner overriding outer; oxygenhub#102). /// Set under `FLOW_*` and under the pipeline's own `env`, so they are /// part of the job's environment and of its receipt. Never secrets: /// those are mounted, not put in the environment (spec §9). #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")] pub env: std::collections::BTreeMap, /// The secrets the job declares (`secret` blocks), as the hub holds /// them for the repository (oxygenhub#107). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub secrets: Vec, } impl Claim { /// What a job is told about what it builds (docs/m0.md §4): /// `FLOW_REPO`, `FLOW_STATE`, `FLOW_HEADS` (full hashes joined with /// `+`), `FLOW_BOOKMARK`, `FLOW_RUN` and `FLOW_JOB`. Unknown values are /// left out. pub fn flow_env(&self) -> Vec<(String, String)> { let mut v = vec![ ("FLOW_REPO".to_string(), self.repo.clone()), ("FLOW_HEADS".to_string(), self.heads.join("+")), ("FLOW_JOB".to_string(), self.job.name.clone()), ]; if !self.state.is_empty() { v.push(("FLOW_STATE".into(), self.state.clone())); } if !self.bookmark.is_empty() { v.push(("FLOW_BOOKMARK".into(), self.bookmark.clone())); } if self.run_number > 0 { v.push(("FLOW_RUN".into(), self.run_number.to_string())); } for (k, val) in &self.env { if !v.iter().any(|(n, _)| n == k) { v.push((k.clone(), val.clone())); } } v } } /// One line of output (docs/m0.md §2). #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Record { /// Milliseconds since the job started. pub t: u64, /// `out`, `err`, `console` (the VM outside the job) or `flow` (the runner). pub stream: String, pub text: String, } /// `POST /api/v1/flow/jobs/{id}/log`. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct LogBatch { pub records: Vec, } /// `POST /api/v1/flow/jobs/{id}/finish`. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Finish { /// The job's exit code; absent when the runner could not run it. pub exit_code: Option, pub duration_ms: u64, pub receipt: Option, /// The receipt bound to the job's state and signed by the runner /// (Oxygen spec §14.6), hex of the signed receipt's bytes. #[serde(default, skip_serializing_if = "Option::is_none")] pub signed_receipt: Option, /// Why the runner could not run the job (rootfs missing, VM failed). pub error: Option, } /// Longest text of one record the hub keeps; longer lines are cut. pub const MAX_RECORD_TEXT: usize = 16 * 1024; /// Most records the hub keeps per job; later ones are dropped with a note. pub const MAX_RECORDS: usize = 50_000;