solstice-ci/crates/orchestrator/src/http.rs
Till Wegmueller 97599eb48d
Move runner logs to debug level and enable runner binary serving via orchestrator
This commit includes:
- Adjusted runner logs from `info` to `debug` for reduced deployment log verbosity while retaining visibility in CI.
- Added functionality to serve runner binaries directly from the orchestrator via HTTP.
- Introduced new `RUNNER_DIR` configuration to specify the binary directory, with default paths and URL composition.
- Updated HTTP routing to include runner file serving with validation and logging.
- Improved AMQP body logging with a utility for better error debugging.
- Updated task scripts for runner cross-building and serving, consolidating configurations and removing redundant files.
2025-11-06 21:44:06 +01:00

94 lines
3.2 KiB
Rust

use axum::{extract::Path, http::StatusCode, response::{IntoResponse, Response}, routing::get, Router};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::fs;
use tracing::{debug, info, warn};
use uuid::Uuid;
use crate::persist::Persist;
#[derive(Clone)]
pub struct HttpState {
persist: Arc<Persist>,
runner_dir: Option<PathBuf>,
}
pub fn build_router(persist: Arc<Persist>, runner_dir: Option<PathBuf>) -> Router {
let state = HttpState { persist, runner_dir };
Router::new()
.route("/jobs/{request_id}/logs", get(get_logs))
.route("/runners/{name}", get(get_runner))
.with_state(state)
}
async fn get_logs(
Path(request_id): Path<String>,
axum::extract::State(state): axum::extract::State<HttpState>,
) -> Response {
let Ok(id) = Uuid::parse_str(&request_id) else {
return StatusCode::BAD_REQUEST.into_response();
};
if !state.persist.is_enabled() {
return (StatusCode::SERVICE_UNAVAILABLE, "persistence disabled").into_response();
}
match state.persist.get_logs_text(id).await {
Ok(Some(text)) => (
StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "text/plain; charset=utf-8")],
text,
)
.into_response(),
Ok(None) => StatusCode::NOT_FOUND.into_response(),
Err(e) => {
warn!(error = %e, request_id = %id, "failed to read logs");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
async fn get_runner(
Path(name): Path<String>,
axum::extract::State(state): axum::extract::State<HttpState>,
) -> Response {
let Some(dir) = state.runner_dir.as_ref() else {
return (StatusCode::SERVICE_UNAVAILABLE, "runner serving disabled").into_response();
};
// Basic validation: prevent path traversal; allow only simple file names
if name.contains('/') || name.contains('\\') || name.starts_with('.') {
return StatusCode::BAD_REQUEST.into_response();
}
let path = dir.join(&name);
if !path.starts_with(dir) {
return StatusCode::BAD_REQUEST.into_response();
}
match fs::read(&path).await {
Ok(bytes) => {
debug!(path = %path.display(), size = bytes.len(), "serving runner binary");
(
StatusCode::OK,
[
(axum::http::header::CONTENT_TYPE, "application/octet-stream"),
(axum::http::header::CONTENT_DISPOSITION, &format!("attachment; filename=\"{}\"", name)),
],
bytes,
)
.into_response()
}
Err(e) => {
debug!(error = %e, path = %path.display(), "runner file not found");
StatusCode::NOT_FOUND.into_response()
}
}
}
pub async fn serve(addr: SocketAddr, persist: Arc<Persist>, runner_dir: Option<PathBuf>, shutdown: impl std::future::Future<Output = ()>) {
let app = build_router(persist, runner_dir);
info!(%addr, "http server starting");
let listener = tokio::net::TcpListener::bind(addr).await.expect("bind http");
let server = axum::serve(listener, app);
let _ = tokio::select! {
_ = server => {},
_ = shutdown => {},
};
}