Author:
Hash:
Timestamp:
+92 -52 +/-7 browse
Kevin Schoon [me@kevinschoon.com]
82efe86b08f79db547dbe4cef35ba27bd30ba021
Mon, 11 May 2026 16:03:45 +0000 (3 days ago)
| 1 | diff --git a/ayllu-build/src/error.rs b/ayllu-build/src/error.rs |
| 2 | index 9296349..873ef1e 100644 |
| 3 | --- a/ayllu-build/src/error.rs |
| 4 | +++ b/ayllu-build/src/error.rs |
| 5 | @@ -1,4 +1,4 @@ |
| 6 | - use std::path::PathBuf; |
| 7 | + use std::{path::PathBuf, time::Duration}; |
| 8 | |
| 9 | #[derive(Debug)] |
| 10 | pub enum Error { |
| 11 | @@ -30,6 +30,7 @@ pub enum Error { |
| 12 | Io(std::io::Error), |
| 13 | Reqwest(reqwest::Error), |
| 14 | Libpod(crate::libpod::ErrorMessage), |
| 15 | + ContainerTimeout(Duration), |
| 16 | } |
| 17 | |
| 18 | impl std::fmt::Display for Error { |
| 19 | @@ -60,6 +61,9 @@ impl std::fmt::Display for Error { |
| 20 | "Libpod API Error: {}: {} {}", |
| 21 | error_message.cause, error_message.message, error_message.response |
| 22 | ), |
| 23 | + Error::ContainerTimeout(duration) => { |
| 24 | + write!(f, "Container took too long to start up: {duration:?}") |
| 25 | + } |
| 26 | } |
| 27 | } |
| 28 | } |
| 29 | diff --git a/ayllu-build/src/libpod.rs b/ayllu-build/src/libpod.rs |
| 30 | index 9d3d6e5..fef0d1f 100644 |
| 31 | --- a/ayllu-build/src/libpod.rs |
| 32 | +++ b/ayllu-build/src/libpod.rs |
| 33 | @@ -1,7 +1,7 @@ |
| 34 | use std::{ |
| 35 | collections::HashMap, |
| 36 | - io::{BufRead, Read}, |
| 37 | path::Path, |
| 38 | + time::{Duration, SystemTime}, |
| 39 | }; |
| 40 | |
| 41 | use serde_json::json; |
| 42 | @@ -47,12 +47,20 @@ pub mod container { |
| 43 | } |
| 44 | |
| 45 | #[derive(Serialize, Debug)] |
| 46 | + pub struct HealthConfig { |
| 47 | + pub test: Vec<String>, |
| 48 | + } |
| 49 | + |
| 50 | + #[derive(Serialize, Debug)] |
| 51 | pub struct Create { |
| 52 | pub name: String, |
| 53 | pub image: String, |
| 54 | pub command: Vec<String>, |
| 55 | pub mounts: Vec<Mount>, |
| 56 | pub terminal: bool, |
| 57 | + pub healthconfig: Option<HealthConfig>, |
| 58 | + #[serde(rename = "startupHealthConfig")] |
| 59 | + pub startup_health_config: Option<HealthConfig>, |
| 60 | } |
| 61 | |
| 62 | #[derive(Serialize, Debug)] |
| 63 | @@ -65,16 +73,16 @@ pub mod container { |
| 64 | pub name: String, |
| 65 | } |
| 66 | |
| 67 | - #[derive(Serialize, Debug)] |
| 68 | - pub struct Attach { |
| 69 | - pub name: String, |
| 70 | - } |
| 71 | - |
| 72 | #[derive(Debug)] |
| 73 | pub struct Copy { |
| 74 | pub name: String, |
| 75 | pub path: String, |
| 76 | } |
| 77 | + |
| 78 | + #[derive(Serialize, Debug)] |
| 79 | + pub struct HealthCheck { |
| 80 | + pub name: String, |
| 81 | + } |
| 82 | } |
| 83 | |
| 84 | pub mod exec { |
| 85 | @@ -105,29 +113,17 @@ pub mod exec { |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | - pub mod images { |
| 90 | - use super::*; |
| 91 | - |
| 92 | - #[derive(Serialize, Debug)] |
| 93 | - pub struct Pull {} |
| 94 | - |
| 95 | - #[derive(Serialize, Debug)] |
| 96 | - pub struct Exists {} |
| 97 | - } |
| 98 | - |
| 99 | #[derive(Debug)] |
| 100 | pub enum Request { |
| 101 | Ping, |
| 102 | CreateContainer(container::Create), |
| 103 | ContainerExists(container::Exists), |
| 104 | - ContainerAttach(container::Attach), |
| 105 | + HealthCheck(container::HealthCheck), |
| 106 | StartContainer(container::Start), |
| 107 | CopyFiles(container::Copy), |
| 108 | CreateExec(exec::Create), |
| 109 | StartExec(exec::Start), |
| 110 | InspectExec(exec::Inspect), |
| 111 | - ImageExists(images::Exists), |
| 112 | - PullImage(images::Pull), |
| 113 | } |
| 114 | |
| 115 | impl Request { |
| 116 | @@ -151,32 +147,6 @@ impl Request { |
| 117 | )) |
| 118 | .unwrap(), |
| 119 | ), |
| 120 | - Request::ContainerAttach(attach) => { |
| 121 | - let headers = HeaderMap::from_iter(vec![ |
| 122 | - ( |
| 123 | - HeaderName::from_static("connection"), |
| 124 | - HeaderValue::from_static("upgrade"), |
| 125 | - ), |
| 126 | - ( |
| 127 | - HeaderName::from_static("upgrade"), |
| 128 | - HeaderValue::from_static("tcp"), |
| 129 | - ), |
| 130 | - ]); |
| 131 | - client |
| 132 | - .post( |
| 133 | - Url::parse_with_params( |
| 134 | - &format!("http://_/v6.0.0/libpod/containers/{}/attach", attach.name), |
| 135 | - &[ |
| 136 | - ("stream", true.to_string()), |
| 137 | - ("stdout", true.to_string()), |
| 138 | - ("stderr", false.to_string()), |
| 139 | - ("stdin", true.to_string()), |
| 140 | - ], |
| 141 | - ) |
| 142 | - .unwrap(), |
| 143 | - ) |
| 144 | - .headers(headers) |
| 145 | - } |
| 146 | Request::CopyFiles(copy) => client.put( |
| 147 | Url::parse_with_params( |
| 148 | &format!("http://_/v6.0.0/libpod/containers/{}/archive", copy.name), |
| 149 | @@ -221,8 +191,13 @@ impl Request { |
| 150 | Request::InspectExec(inspect) => client.get( |
| 151 | Url::parse(&format!("http://_/v6.0.0/libpod/exec/{}/json", inspect.id)).unwrap(), |
| 152 | ), |
| 153 | - Request::ImageExists(exists) => todo!(), |
| 154 | - Request::PullImage(pull) => todo!(), |
| 155 | + Request::HealthCheck(check) => client.get( |
| 156 | + Url::parse(&format!( |
| 157 | + "http://_/v6.0.0/libpod/containers/{}/healthcheck", |
| 158 | + check.name |
| 159 | + )) |
| 160 | + .unwrap(), |
| 161 | + ), |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | @@ -320,6 +295,30 @@ impl Client { |
| 166 | Ok(res) |
| 167 | } |
| 168 | |
| 169 | + /// Wait for the container to become healthy |
| 170 | + /// NOTE: Podman is daemonless and thus these calls drive the healthchecks |
| 171 | + /// forward. |
| 172 | + pub async fn wait(&self, name: &str, timeout: Duration) -> Result<(), Error> { |
| 173 | + let start = SystemTime::now(); |
| 174 | + while SystemTime::now().duration_since(start).unwrap() < timeout { |
| 175 | + let res = self |
| 176 | + .call(&Request::HealthCheck(container::HealthCheck { |
| 177 | + name: name.to_string(), |
| 178 | + })) |
| 179 | + .await?; |
| 180 | + let res = res.json::<serde_json::Value>().await?; |
| 181 | + let res = res.as_object().unwrap(); |
| 182 | + let status = res.get("Status").unwrap(); |
| 183 | + if status.as_str() == Some("healthy") { |
| 184 | + return Ok(()); |
| 185 | + } |
| 186 | + tokio::time::sleep(Duration::from_millis(250)).await; |
| 187 | + } |
| 188 | + Err(Error::ContainerTimeout( |
| 189 | + SystemTime::now().duration_since(start).unwrap(), |
| 190 | + )) |
| 191 | + } |
| 192 | + |
| 193 | pub async fn exec( |
| 194 | &self, |
| 195 | ctx: &Context, |
| 196 | diff --git a/ayllu-build/src/libpod_executor.rs b/ayllu-build/src/libpod_executor.rs |
| 197 | index f229923..f432fd0 100644 |
| 198 | --- a/ayllu-build/src/libpod_executor.rs |
| 199 | +++ b/ayllu-build/src/libpod_executor.rs |
| 200 | @@ -1,3 +1,4 @@ |
| 201 | + use std::time::Duration; |
| 202 | use std::{collections::HashMap, path::Path}; |
| 203 | |
| 204 | use tokio::sync::{mpsc, oneshot}; |
| 205 | @@ -6,6 +7,7 @@ use ayllu_database::build::logs::{Stream, WriteLineArgs}; |
| 206 | use ayllu_database::build::steps::Step; |
| 207 | use serde::Deserialize; |
| 208 | |
| 209 | + use crate::libpod::container::HealthConfig; |
| 210 | use crate::libpod::{ |
| 211 | Client, Request, |
| 212 | container::{self, Mount}, |
| 213 | @@ -92,6 +94,13 @@ impl Libpod { |
| 214 | }, |
| 215 | ], |
| 216 | terminal: true, |
| 217 | + healthconfig: Some(HealthConfig { |
| 218 | + test: vec!["/usr/bin/ayllu-init".to_string(), "--check".to_string()], |
| 219 | + }), |
| 220 | + startup_health_config: None, |
| 221 | + // startup_health_config: Some(HealthConfig { |
| 222 | + // test: vec!["/usr/bin/ayllu-init".to_string(), "--check".to_string()], |
| 223 | + // }), |
| 224 | })) |
| 225 | .await?; |
| 226 | } |
| 227 | @@ -106,6 +115,10 @@ impl Libpod { |
| 228 | })) |
| 229 | .await?; |
| 230 | |
| 231 | + self.client |
| 232 | + .wait(&container_name, Duration::from_secs(60)) |
| 233 | + .await?; |
| 234 | + |
| 235 | Ok(()) |
| 236 | } |
| 237 | |
| 238 | diff --git a/ayllu-init/src/main.rs b/ayllu-init/src/main.rs |
| 239 | index 4151c98..02055fa 100644 |
| 240 | --- a/ayllu-init/src/main.rs |
| 241 | +++ b/ayllu-init/src/main.rs |
| 242 | @@ -1,4 +1,4 @@ |
| 243 | - use std::path::Path; |
| 244 | + use std::{path::Path, time::Duration}; |
| 245 | |
| 246 | use tokio::signal::unix::{SignalKind, signal}; |
| 247 | |
| 248 | @@ -8,6 +8,8 @@ use ayllu_cmd::init::Command; |
| 249 | const SOURCE_BUNDLE_PATH: &str = "/_ayllu/src.bundle"; |
| 250 | /// Working path builds are executed in, must exist with adequate permissions |
| 251 | const SOURCE_BUILD_PATH: &str = "/src"; |
| 252 | + /// Checked to determine if the container has been initialized |
| 253 | + const AYLLU_BUILD_READY: &str = "/tmp/ayllu-build-ready"; |
| 254 | |
| 255 | fn within_ayllu_build_container() -> bool { |
| 256 | // TODO: more checks |
| 257 | @@ -20,13 +22,30 @@ fn initialize() -> Result<(), Box<dyn std::error::Error>> { |
| 258 | if bundle_path.exists() { |
| 259 | ayllu_git::clone(SOURCE_BUNDLE_PATH, Path::new(SOURCE_BUILD_PATH), None)?; |
| 260 | } |
| 261 | + std::fs::write(Path::new(AYLLU_BUILD_READY), "1")?; |
| 262 | Ok(()) |
| 263 | } |
| 264 | |
| 265 | + async fn ready() -> Result<(), Box<dyn std::error::Error>> { |
| 266 | + let path = Path::new(AYLLU_BUILD_READY); |
| 267 | + loop { |
| 268 | + if path.exists() { |
| 269 | + return Ok(()); |
| 270 | + } |
| 271 | + tokio::time::sleep(Duration::from_millis(350)).await; |
| 272 | + } |
| 273 | + } |
| 274 | + |
| 275 | #[tokio::main(flavor = "current_thread")] |
| 276 | async fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 277 | - let _ = ayllu_cmd::parse::<Command>(); |
| 278 | + let args = ayllu_cmd::parse::<Command>(); |
| 279 | ayllu_logging::init(ayllu_logging::Level::INFO); |
| 280 | + if args.check { |
| 281 | + tracing::info!("Waiting for build environment to be ready"); |
| 282 | + ready().await?; |
| 283 | + tracing::info!("Environment is ready!"); |
| 284 | + return Ok(()); |
| 285 | + } |
| 286 | if !within_ayllu_build_container() { |
| 287 | return Err("ayllu-init can only be run from within a build container".into()); |
| 288 | }; |
| 289 | diff --git a/ayllu-web/templates/builds.html b/ayllu-web/templates/builds.html |
| 290 | index 717cea4..c20bdc4 100644 |
| 291 | --- a/ayllu-web/templates/builds.html |
| 292 | +++ b/ayllu-web/templates/builds.html |
| 293 | @@ -24,7 +24,7 @@ |
| 294 | </td> |
| 295 | <td> |
| 296 | <a href="/{{collection}}/{{name}}/builds/{{manifest.id}}"> |
| 297 | - ??? |
| 298 | + {{ manifest.state }} |
| 299 | </a> |
| 300 | </td> |
| 301 | <td>{{ manifest.created_at | friendly_time_32 }}</td> |
| 302 | diff --git a/crates/cmd/src/init.rs b/crates/cmd/src/init.rs |
| 303 | index 4e2b54e..7dbd78b 100644 |
| 304 | --- a/crates/cmd/src/init.rs |
| 305 | +++ b/crates/cmd/src/init.rs |
| 306 | @@ -9,4 +9,8 @@ is responsible for initializing the build environment. |
| 307 | /// ayllu-init process |
| 308 | #[derive(Parser, Debug)] |
| 309 | #[clap(version, about, name = "ayllu-init", long_about = LONG_ABOUT_DESCRIPTION)] |
| 310 | - pub struct Command {} |
| 311 | + pub struct Command { |
| 312 | + /// Check if the build environment is ready and then exit normally |
| 313 | + #[clap(short, long, action)] |
| 314 | + pub check: bool, |
| 315 | + } |
| 316 | diff --git a/crates/database/src/build.rs b/crates/database/src/build.rs |
| 317 | index aedac2d..ce26d4b 100644 |
| 318 | --- a/crates/database/src/build.rs |
| 319 | +++ b/crates/database/src/build.rs |
| 320 | @@ -148,6 +148,7 @@ pub mod manifests { |
| 321 | .select(Manifest::as_select()) |
| 322 | .filter(dsl::collection.eq(collection)) |
| 323 | .filter(dsl::name.eq(name)) |
| 324 | + .order_by(dsl::id.desc()) |
| 325 | .load(self.0) |
| 326 | } |
| 327 |