| 1 | //! The wire format between anvil and a job runner (see |
| 2 | //! `docs/remote-runners.md`). |
| 3 | //! |
| 4 | //! Its own crate, depending on nothing but serde, so the runner binary does not |
| 5 | //! link `anvil-core` (and therefore toasty, SQLite, gix and the rest of the |
| 6 | //! forge) just to learn the shape of a job. |
| 7 | //! |
| 8 | //! The split it encodes: anvil resolves the repo, parses `.anvil/ci.yml`, |
| 9 | //! checks the image allowlist, opens the secret vault and assembles the shell |
| 10 | //! script. A runner receives an image, a script, a tar and some limits, and |
| 11 | //! never parses a pipeline. That keeps this format stable as the pipeline |
| 12 | //! schema grows. |
| 13 | |
| 14 | use serde::{ |
| 15 | Deserialize, |
| 16 | Serialize, |
| 17 | }; |
| 18 | |
| 19 | /// Working directory inside the job container, and the root the checkout tar |
| 20 | /// extracts to. Protocol, not preference: artifact paths are resolved relative |
| 21 | /// to it on the runner, and `build_tar` roots its entries at it on the server. |
| 22 | pub const WORKDIR: &str = "/workspace"; |
| 23 | |
| 24 | /// In-container directory where meta-extractor output lands, one file per |
| 25 | /// `<artifact>/<key>`. The server writes the redirections into the script; the |
| 26 | /// runner downloads the directory afterwards. File-per-value sidesteps |
| 27 | /// quoting and JSON-escaping in shell entirely. |
| 28 | pub const META_DIR: &str = "/tmp/anvil-meta"; |
| 29 | |
| 30 | /// Cap on the meta-extractor tar (the values are short strings). |
| 31 | pub const META_TAR_CAP: u64 = 1024 * 1024; |
| 32 | |
| 33 | /// Per-value cap on extractor output, in bytes (after trimming). |
| 34 | pub const META_VALUE_CAP: usize = 1024; |
| 35 | |
| 36 | /// What a runner says about itself when it asks for work. |
| 37 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 38 | pub struct RunnerInfo { |
| 39 | /// Operator-chosen, for logs and (later) scheduling. Not a credential. |
| 40 | pub name: String, |
| 41 | /// The daemon's native platform, e.g. `linux/arm64`. Advertised so the |
| 42 | /// dispatcher can eventually prefer a runner that needs no emulation. |
| 43 | pub platform: String, |
| 44 | pub version: String, |
| 45 | } |
| 46 | |
| 47 | /// One artifact to collect, flattened from `anvil_core::ci::ArtifactSpec`. |
| 48 | /// |
| 49 | /// The extractor commands themselves are not here — the server has already |
| 50 | /// baked them into the script. All the runner needs to know is whether to |
| 51 | /// expect output for this artifact under [`META_DIR`]. |
| 52 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 53 | pub struct ArtifactSpec { |
| 54 | pub name: String, |
| 55 | /// Relative to [`WORKDIR`]. |
| 56 | pub path: String, |
| 57 | pub browse: bool, |
| 58 | pub has_meta: bool, |
| 59 | } |
| 60 | |
| 61 | /// Resource bounds and isolation knobs, resolved from `[ci]` server-side. |
| 62 | /// |
| 63 | /// The runner obeys these; it does not consult a config file of its own for |
| 64 | /// them. An operator tightening `ci.memory_mb` should not have to redeploy |
| 65 | /// every runner. |
| 66 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 67 | pub struct Sandbox { |
| 68 | pub memory_mb: i64, |
| 69 | pub cpus: f64, |
| 70 | pub pids_limit: i64, |
| 71 | /// 0 disables the wall-clock timeout. |
| 72 | pub timeout_secs: u64, |
| 73 | pub network: bool, |
| 74 | /// Empty keeps the image's default user. |
| 75 | pub run_as: String, |
| 76 | /// Per-artifact and per-run artifact caps, in MiB; 0 is unlimited. |
| 77 | /// |
| 78 | /// The per-run budget is charged the *stored* size, which the runner learns |
| 79 | /// from the upload response rather than computing — a `browse` directory is |
| 80 | /// extracted and any other directory recompressed, so the tar it sent is |
| 81 | /// not what lands on disk. Same accounting as the in-process runner did. |
| 82 | pub artifact_max_mb: i64, |
| 83 | pub artifact_run_max_mb: i64, |
| 84 | } |
| 85 | |
| 86 | /// A claimed job, everything needed to run it. |
| 87 | /// |
| 88 | /// The checkout is fetched separately (`GET /-/runner/jobs/{run_id}/checkout.tar`) |
| 89 | /// rather than carried here: base64 in a JSON body inflates a large tree by a |
| 90 | /// third for no benefit. Secrets *are* carried here, over TLS, so they never |
| 91 | /// sit behind a separately-fetchable URL. |
| 92 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 93 | pub struct JobSpec { |
| 94 | pub run_id: i64, |
| 95 | /// Already resolved through `ci.default_image` and checked against |
| 96 | /// `ci.allowed_images`. |
| 97 | pub image: String, |
| 98 | /// `linux/amd64`, `linux/arm64`, … `None` takes the daemon's native |
| 99 | /// platform, which is the current behaviour everywhere. |
| 100 | pub platform: Option<String>, |
| 101 | /// The full `sh -c` program: steps, then the meta-extractor trailer. |
| 102 | pub script: String, |
| 103 | /// Secret name/value pairs, injected as environment variables. |
| 104 | pub env: Vec<(String, String)>, |
| 105 | pub artifacts: Vec<ArtifactSpec>, |
| 106 | pub sandbox: Sandbox, |
| 107 | } |
| 108 | |
| 109 | /// What storing one artifact produced. Returned by the upload endpoint, and by |
| 110 | /// the in-process sink, so the runner can charge the run budget the size that |
| 111 | /// actually landed on disk without knowing how it was laid out. |
| 112 | #[derive(Clone, Copy, Debug, Serialize, Deserialize)] |
| 113 | pub struct Stored { |
| 114 | pub size: i64, |
| 115 | pub is_dir: bool, |
| 116 | } |
| 117 | |
| 118 | /// One artifact the runner pulled out of the container and uploaded. The bytes |
| 119 | /// went ahead of this; here is the row to record for them. |
| 120 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 121 | pub struct CollectedArtifact { |
| 122 | pub name: String, |
| 123 | pub size: i64, |
| 124 | pub is_dir: bool, |
| 125 | pub browse: bool, |
| 126 | /// Extractor output for this artifact: a JSON object of key → value. |
| 127 | pub meta: String, |
| 128 | } |
| 129 | |
| 130 | /// The outcome of a job, posted once when it finishes. |
| 131 | /// |
| 132 | /// The whole log arrives in one write, which is exactly what the in-process |
| 133 | /// runner did — `append_log` was only ever called when the run ended. Live |
| 134 | /// logs are a follow-up, not a regression. |
| 135 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 136 | pub struct JobResult { |
| 137 | pub exit_code: i64, |
| 138 | pub log: String, |
| 139 | pub artifacts: Vec<CollectedArtifact>, |
| 140 | /// The runner could not run the job at all (image pull failed, daemon |
| 141 | /// unreachable, timeout). Distinct from a job that ran and exited |
| 142 | /// non-zero: this maps to `status::ERROR`, that to `status::FAILURE`. |
| 143 | pub runner_error: Option<String>, |
| 144 | } |