Commit

Author:

Hash:

Timestamp:

+92 -52 +/-7 browse

Kevin Schoon [me@kevinschoon.com]

82efe86b08f79db547dbe4cef35ba27bd30ba021

Mon, 11 May 2026 16:03:45 +0000 (3 days ago)

wire up healthceck for build containers
1diff --git a/ayllu-build/src/error.rs b/ayllu-build/src/error.rs
2index 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
30index 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
197index 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
239index 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
290index 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
303index 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
317index 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