Author:
Hash:
Timestamp:
+94 -39 +/-4 browse
Kevin Schoon [me@kevinschoon.com]
cc91cc3c7d9cd5eb36b77cbdc373b46212cd08fe
Fri, 15 May 2026 08:31:02 +0000 (3 weeks ago)
| 1 | diff --git a/ayllu-build/src/libpod.rs b/ayllu-build/src/libpod.rs |
| 2 | index fef0d1f..a1cd647 100644 |
| 3 | --- a/ayllu-build/src/libpod.rs |
| 4 | +++ b/ayllu-build/src/libpod.rs |
| 5 | @@ -30,6 +30,15 @@ pub enum Stream { |
| 6 | Stderr, |
| 7 | } |
| 8 | |
| 9 | + impl From<ayllu_database::build::logs::Stream> for Stream { |
| 10 | + fn from(value: ayllu_database::build::logs::Stream) -> Self { |
| 11 | + match value { |
| 12 | + ayllu_database::build::logs::Stream::Stdout => Stream::Stdout, |
| 13 | + ayllu_database::build::logs::Stream::Stderr => Stream::Stderr, |
| 14 | + } |
| 15 | + } |
| 16 | + } |
| 17 | + |
| 18 | pub mod container { |
| 19 | |
| 20 | use super::*; |
| 21 | @@ -324,7 +333,7 @@ impl Client { |
| 22 | ctx: &Context, |
| 23 | shell: &str, |
| 24 | input: &str, |
| 25 | - ch: Sender<(Stream, String)>, |
| 26 | + ch: Sender<(Stream, Vec<u8>)>, |
| 27 | signal: tokio::sync::oneshot::Sender<i32>, |
| 28 | ) -> Result<(), Error> { |
| 29 | let name = format!("ayllu-build-{}-{}", ctx.manifest_id, ctx.workflow_id); |
| 30 | @@ -347,20 +356,21 @@ impl Client { |
| 31 | match reader.read_exact(&mut header).await { |
| 32 | Ok(_) => { |
| 33 | let (stream, size) = Header::read(&header); |
| 34 | - tracing::debug!("read stream: {stream:?} {size}"); |
| 35 | + tracing::trace!("read stream: {stream:?} {size}"); |
| 36 | let mut buf = vec![0; size as usize]; |
| 37 | - reader.read_exact(buf.as_mut_slice()).await.unwrap(); |
| 38 | - for line in buf.split(|&b| b == b'\n') { |
| 39 | - let line = if line.last() == Some(&b'\r') { |
| 40 | - &line[..line.len() - 1] |
| 41 | - } else { |
| 42 | - line |
| 43 | - }; |
| 44 | - if !line.is_empty() { |
| 45 | - let line = String::from_utf8_lossy(line); |
| 46 | - ch.send((stream, line.to_string())).await.unwrap() |
| 47 | - } |
| 48 | - } |
| 49 | + reader.read_exact(buf.as_mut_slice()).await?; |
| 50 | + ch.send((stream, buf)).await.unwrap(); |
| 51 | + // for line in buf.split(|&b| b == b'\n') { |
| 52 | + // let line = if line.last() == Some(&b'\r') { |
| 53 | + // &line[..line.len() - 1] |
| 54 | + // } else { |
| 55 | + // line |
| 56 | + // }; |
| 57 | + // if !line.is_empty() { |
| 58 | + // let line = String::from_utf8_lossy(line); |
| 59 | + // ch.send((stream, line.to_string())).await.unwrap() |
| 60 | + // } |
| 61 | + // } |
| 62 | } |
| 63 | Err(e) => match e.kind() { |
| 64 | tokio::io::ErrorKind::UnexpectedEof => break, |
| 65 | diff --git a/ayllu-build/src/libpod_executor.rs b/ayllu-build/src/libpod_executor.rs |
| 66 | index 4bb6d46..317ab9a 100644 |
| 67 | --- a/ayllu-build/src/libpod_executor.rs |
| 68 | +++ b/ayllu-build/src/libpod_executor.rs |
| 69 | @@ -1,3 +1,4 @@ |
| 70 | + use std::io::{BufRead, Write}; |
| 71 | use std::time::Duration; |
| 72 | use std::{collections::HashMap, path::Path}; |
| 73 | |
| 74 | @@ -6,6 +7,7 @@ use tokio::sync::{mpsc, oneshot}; |
| 75 | use ayllu_database::build::logs::{Stream, WriteLineArgs}; |
| 76 | use ayllu_database::build::steps::Step; |
| 77 | use serde::Deserialize; |
| 78 | + use tracing::Level; |
| 79 | |
| 80 | use crate::libpod::container::HealthConfig; |
| 81 | use crate::libpod::{ |
| 82 | @@ -39,12 +41,20 @@ pub struct InitializeArgs<'a> { |
| 83 | |
| 84 | pub struct Libpod { |
| 85 | client: Client, |
| 86 | + level: tracing::Level, |
| 87 | + forward: bool, |
| 88 | } |
| 89 | |
| 90 | impl Libpod { |
| 91 | - pub fn new(socket_path: &Path) -> Self { |
| 92 | + pub fn new(socket_path: &Path, forward: bool, use_logger: bool) -> Self { |
| 93 | Self { |
| 94 | client: Client::new(socket_path), |
| 95 | + level: if use_logger { |
| 96 | + tracing::Level::INFO |
| 97 | + } else { |
| 98 | + tracing::Level::TRACE |
| 99 | + }, |
| 100 | + forward, |
| 101 | } |
| 102 | } |
| 103 | } |
| 104 | @@ -128,7 +138,7 @@ impl Libpod { |
| 105 | context: &Context, |
| 106 | db: &mut ayllu_database::Ptr<'a>, |
| 107 | ) -> Result<i32, crate::error::Error> { |
| 108 | - let (tx, mut rx) = mpsc::channel::<(crate::libpod::Stream, String)>(1); |
| 109 | + let (tx, mut rx) = mpsc::channel::<(crate::libpod::Stream, Vec<u8>)>(1); |
| 110 | let (tx_signal, rx_signal) = oneshot::channel::<i32>(); |
| 111 | let shell = step.shell.to_string(); |
| 112 | let input = step.input.to_string(); |
| 113 | @@ -140,17 +150,39 @@ impl Libpod { |
| 114 | } |
| 115 | }); |
| 116 | while let Some(msg) = rx.recv().await { |
| 117 | - let stream = match msg.0 { |
| 118 | - crate::libpod::Stream::Stdin => unimplemented!(), |
| 119 | - crate::libpod::Stream::Stdout => Stream::Stdout, |
| 120 | - crate::libpod::Stream::Stderr => Stream::Stderr, |
| 121 | + let (stream, chunk) = match &msg { |
| 122 | + (crate::libpod::Stream::Stdin, _) => unimplemented!(), |
| 123 | + (crate::libpod::Stream::Stdout, chunk) => (Stream::Stdout, chunk), |
| 124 | + (crate::libpod::Stream::Stderr, chunk) => (Stream::Stderr, chunk), |
| 125 | }; |
| 126 | - tracing::trace!(stream = stream.to_string(), message = msg.1.trim_end()); |
| 127 | - db.log_write_line(&WriteLineArgs { |
| 128 | - step_id: step.id, |
| 129 | - stream, |
| 130 | - line: &msg.1, |
| 131 | - })?; |
| 132 | + if self.forward { |
| 133 | + match stream { |
| 134 | + Stream::Stdout => std::io::stdout().write_all(chunk).unwrap(), |
| 135 | + Stream::Stderr => std::io::stderr().write_all(chunk).unwrap(), |
| 136 | + } |
| 137 | + }; |
| 138 | + let mut buf = std::io::Cursor::new(chunk); |
| 139 | + 'reader: loop { |
| 140 | + let mut line = String::default(); |
| 141 | + if let Ok(n) = buf.read_line(&mut line) { |
| 142 | + let line = line.trim_end_matches("\n"); |
| 143 | + if matches!(self.level, Level::INFO) { |
| 144 | + tracing::info!(stream = format!("{}", stream), "{}", line); |
| 145 | + } else { |
| 146 | + tracing::trace!(stream = format!("{}", stream), "{}", line); |
| 147 | + }; |
| 148 | + db.log_write_line(&WriteLineArgs { |
| 149 | + step_id: step.id, |
| 150 | + stream, |
| 151 | + line, |
| 152 | + })?; |
| 153 | + if n == 0 { |
| 154 | + break 'reader; |
| 155 | + } |
| 156 | + } else { |
| 157 | + break 'reader; |
| 158 | + } |
| 159 | + } |
| 160 | } |
| 161 | |
| 162 | let signal = rx_signal.await.unwrap(); |
| 163 | @@ -160,7 +192,6 @@ impl Libpod { |
| 164 | tracing::warn!("Non-zero exit code: {signal}") |
| 165 | } |
| 166 | |
| 167 | - // self.client.call().await.unwrap(); |
| 168 | Ok(signal) |
| 169 | } |
| 170 | |
| 171 | diff --git a/ayllu-build/src/main.rs b/ayllu-build/src/main.rs |
| 172 | index 33ec53d..2598196 100644 |
| 173 | --- a/ayllu-build/src/main.rs |
| 174 | +++ b/ayllu-build/src/main.rs |
| 175 | @@ -5,7 +5,7 @@ use tracing::Level; |
| 176 | use ayllu_cmd::build::{Command, Commands}; |
| 177 | use ayllu_database::Wrapper as Database; |
| 178 | |
| 179 | - use crate::{config::Config, evaluate::Runtime, source::Source}; |
| 180 | + use crate::{evaluate::Runtime, source::Source}; |
| 181 | |
| 182 | mod config; |
| 183 | mod error; |
| 184 | @@ -25,6 +25,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 185 | source, |
| 186 | stdin, |
| 187 | name, |
| 188 | + logging, |
| 189 | } => { |
| 190 | let source = if stdin { |
| 191 | tracing::info!("Reading source from stdin for build {name:?}"); |
| 192 | @@ -50,7 +51,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 193 | work_dir: workdir.clone(), |
| 194 | init_path: cfg.builder.init_binary.clone(), |
| 195 | }; |
| 196 | - let executor = libpod_executor::Libpod::new(&cfg.builder.podman_socket); |
| 197 | + let executor = libpod_executor::Libpod::new( |
| 198 | + &cfg.builder.podman_socket, |
| 199 | + logging.forward, |
| 200 | + logging.logged, |
| 201 | + ); |
| 202 | rt.evaluate(&cfg, &executor).await?; |
| 203 | Ok(()) |
| 204 | } |
| 205 | diff --git a/crates/cmd/src/build.rs b/crates/cmd/src/build.rs |
| 206 | index 4eb74b8..d4360c1 100644 |
| 207 | --- a/crates/cmd/src/build.rs |
| 208 | +++ b/crates/cmd/src/build.rs |
| 209 | @@ -2,19 +2,24 @@ use std::path::PathBuf; |
| 210 | |
| 211 | use clap::{Parser, Subcommand}; |
| 212 | use tracing::Level; |
| 213 | - use url::Url; |
| 214 | |
| 215 | const LONG_ABOUT_DESCRIPTION: &str = r#" |
| 216 | |
| 217 | ayllu-build provides a continuous integration server that works both locally |
| 218 | and on a remote server. |
| 219 | + |
| 220 | + When --logged is enabled log output is displayed through the standard logger |
| 221 | + along with the corresponding step, workflow id, and stream type. |
| 222 | + |
| 223 | + When --forward is enabled the stdout/stderr of the step process is forwarded |
| 224 | + directly to your terminal. |
| 225 | "#; |
| 226 | |
| 227 | #[derive(Parser, Debug)] |
| 228 | #[clap( |
| 229 | version, |
| 230 | about, |
| 231 | - name = "ayllu-shell", |
| 232 | + name = "ayllu-build", |
| 233 | long_about=LONG_ABOUT_DESCRIPTION, |
| 234 | )] |
| 235 | pub struct Command { |
| 236 | @@ -30,20 +35,24 @@ pub struct Command { |
| 237 | pub command: Commands, |
| 238 | } |
| 239 | |
| 240 | - pub enum Source { |
| 241 | - LocalPath(PathBuf), |
| 242 | - RemoteUrl(Url), |
| 243 | - } |
| 244 | - |
| 245 | - pub enum Environment { |
| 246 | - Local, |
| 247 | - Libpod, |
| 248 | + #[derive(clap::Args, Debug, PartialEq)] |
| 249 | + #[group(required = false, multiple = false)] |
| 250 | + pub struct Logging { |
| 251 | + /// step stream is logged via the internal logger |
| 252 | + #[clap(short, long, action)] |
| 253 | + pub logged: bool, |
| 254 | + /// step stream will be forwarded directly to the terminal |
| 255 | + #[clap(short, long, action)] |
| 256 | + pub forward: bool, |
| 257 | } |
| 258 | |
| 259 | #[derive(Subcommand, Debug, PartialEq)] |
| 260 | pub enum Commands { |
| 261 | /// evaluate a local build script |
| 262 | Evaluate { |
| 263 | + /// Configures build logging |
| 264 | + #[clap(flatten)] |
| 265 | + logging: Logging, |
| 266 | /// Read the collection / name from stdin |
| 267 | #[arg(short, long, action)] |
| 268 | stdin: bool, |