From 930efe547f4b914f8c676da051b5a3e3acb72e496b0d2d397cfd0c942505c0b9 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Tue, 11 Nov 2025 20:24:20 +0100 Subject: [PATCH] Add public runner URL configuration and enhance log streaming support - Introduce options for specifying public runner base URLs (`SOLSTICE_RUNNER_BASE_URL`) and orchestrator contact addresses (`ORCH_CONTACT_ADDR`). - Update `.env.sample` and `compose.yml` with new configuration fields for external log streaming and runner binary serving. - Refactor runner URL handling and generation logic for improved flexibility. - Enhance `cloud-init` templates with updated runner URL environment variables (`RUNNER_SINGLE` and `RUNNER_URLS`). - Add unit tests for runner URL generation to verify various input cases. Signed-off-by: Till Wegmueller --- crates/orchestrator/src/main.rs | 88 ++++++++++++++++++++++------ crates/orchestrator/src/scheduler.rs | 1 + deploy/podman/.env.sample | 10 ++++ deploy/podman/compose.yml | 4 ++ 4 files changed, 84 insertions(+), 19 deletions(-) diff --git a/crates/orchestrator/src/main.rs b/crates/orchestrator/src/main.rs index 700e1e8..335974d 100644 --- a/crates/orchestrator/src/main.rs +++ b/crates/orchestrator/src/main.rs @@ -49,6 +49,16 @@ struct Opts { #[arg(long, env = "GRPC_ADDR", default_value = "0.0.0.0:50051")] grpc_addr: String, + /// Public contact address for runners to stream logs to (host:port). Overrides detection. + #[arg(long = "orch-contact-addr", env = "ORCH_CONTACT_ADDR")] + orch_contact_addr: Option, + + /// Public base URL where runner binaries are served (preferred). Example: https://runner.svc.domain + /// The orchestrator will append /runners/{filename} to construct full URLs. + #[arg(long = "runner-base-url", env = "SOLSTICE_RUNNER_BASE_URL")] + runner_base_url: Option, + + /// Postgres connection string (if empty, persistence is disabled) #[arg( long, @@ -152,9 +162,9 @@ async fn main() -> Result<()> { }); // Orchestrator contact address for runner to dial back (auto-detect if not provided) - let orch_contact = match std::env::var("ORCH_CONTACT_ADDR") { - Ok(v) => v, - Err(_) => detect_contact_addr(&opts), + let orch_contact = match &opts.orch_contact_addr { + Some(v) if !v.trim().is_empty() => v.clone(), + _ => detect_contact_addr(&opts), }; info!(contact = %orch_contact, "orchestrator contact address determined"); @@ -167,12 +177,14 @@ async fn main() -> Result<()> { .rsplit(':') .next() .unwrap_or("8081"); - let base = format!("http://{}:{}/runners", http_host, http_port); - let linux_url = format!("{}/{}", base, "solstice-runner-linux"); - let illumos_url = format!("{}/{}", base, "solstice-runner-illumos"); - let single_url = format!("{}/{}", base, "solstice-runner"); + let base = format!("http://{}:{}", http_host, http_port); + let (single_url, multi_urls) = build_runner_urls(&base); + // Log concrete OS URLs for local serving + let mut parts = multi_urls.split_whitespace(); + let linux_url = parts.next().unwrap_or(""); + let illumos_url = parts.next().unwrap_or(""); info!(linux = %linux_url, illumos = %illumos_url, "serving runner binaries via orchestrator HTTP"); - (single_url, format!("{} {}", linux_url, illumos_url)) + (single_url, multi_urls) } else { (String::new(), String::new()) }; @@ -195,9 +207,20 @@ async fn main() -> Result<()> { } }); - // Determine runner URLs to inject into cloud-init - let runner_url_env = std::env::var("SOLSTICE_RUNNER_URL").unwrap_or_else(|_| runner_url_default.clone()); - let runner_urls_env = std::env::var("SOLSTICE_RUNNER_URLS").unwrap_or_else(|_| runner_urls_default.clone()); + // Determine runner URLs to inject into cloud-init (prefer base URL) + let (runner_url_env, runner_urls_env) = if let Some(base) = opts + .runner_base_url + .as_ref() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + { + let (u1, u2) = build_runner_urls(base); + info!(base = %base, url = %u1, urls = %u2, "using public runner base URL"); + (u1, u2) + } else { + // Fall back to URLs served by this orchestrator's HTTP (when RUNNER_DIR configured) + (runner_url_default.clone(), runner_urls_default.clone()) + }; // Consumer: enqueue and ack-on-accept let cfg_clone = cfg.clone(); @@ -435,6 +458,20 @@ fn extract_attr_value<'a>(tag: &'a str, key: &'a str) -> Option<&'a str> { None } +fn build_runner_urls(base: &str) -> (String, String) { + // Normalize base and ensure it ends with /runners + let trimmed = base.trim_end_matches('/'); + let base = if trimmed.ends_with("/runners") { + trimmed.to_string() + } else { + format!("{}/runners", trimmed) + }; + let single = format!("{}/{}", base, "solstice-runner"); + let linux = format!("{}/{}", base, "solstice-runner-linux"); + let illumos = format!("{}/{}", base, "solstice-runner-illumos"); + (single, format!("{} {}", linux, illumos)) +} + fn make_cloud_init_userdata( repo_url: &str, commit_sha: &str, @@ -461,8 +498,8 @@ write_files: echo "Solstice: bootstrapping workflow runner for {sha}" | tee /dev/console RUNNER="/usr/local/bin/solstice-runner" # Runner URL(s) provided by orchestrator (local dev) if set - export SOLSTICE_RUNNER_URL='{runner_url}' - export SOLSTICE_RUNNER_URLS='{runner_urls}' + RUNNER_SINGLE='{runner_url}' + RUNNER_URLS='{runner_urls}' if [ ! -x "$RUNNER" ]; then mkdir -p /usr/local/bin # Helper to download from a URL to $RUNNER @@ -481,12 +518,12 @@ write_files: }} OS=$(uname -s 2>/dev/null || echo unknown) # Prefer single URL if provided - if [ -n "$SOLSTICE_RUNNER_URL" ]; then - fetch_runner "$SOLSTICE_RUNNER_URL" || true + if [ -n "$RUNNER_SINGLE" ]; then + fetch_runner "$RUNNER_SINGLE" || true fi # If still missing, iterate URLs with a basic OS-based preference - if [ ! -x "$RUNNER" ] && [ -n "$SOLSTICE_RUNNER_URLS" ]; then - for U in $SOLSTICE_RUNNER_URLS; do + if [ ! -x "$RUNNER" ] && [ -n "$RUNNER_URLS" ]; then + for U in $RUNNER_URLS; do case "$OS" in Linux) echo "$U" | grep -qi linux || continue ;; @@ -498,8 +535,8 @@ write_files: done fi # As a final fallback, try all URLs regardless of OS tag - if [ ! -x "$RUNNER" ] && [ -n "$SOLSTICE_RUNNER_URLS" ]; then - for U in $SOLSTICE_RUNNER_URLS; do + if [ ! -x "$RUNNER" ] && [ -n "$RUNNER_URLS" ]; then + for U in $RUNNER_URLS; do fetch_runner "$U" && break || true done fi @@ -549,6 +586,19 @@ mod tests { assert!(m.get("other").is_none()); } + #[test] + fn test_build_runner_urls_variants() { + let (s, m) = super::build_runner_urls("https://runner.svc.example"); + assert_eq!(s, "https://runner.svc.example/runners/solstice-runner"); + assert_eq!(m, "https://runner.svc.example/runners/solstice-runner-linux https://runner.svc.example/runners/solstice-runner-illumos"); + let (s2, m2) = super::build_runner_urls("https://runner.svc.example/"); + assert_eq!(s2, s); + assert_eq!(m2, m); + let (s3, m3) = super::build_runner_urls("https://runner.svc.example/runners"); + assert_eq!(s3, s); + assert_eq!(m3, m); + } + #[test] fn test_make_cloud_init_userdata_includes_fields() { let req_id = uuid::Uuid::new_v4(); diff --git a/crates/orchestrator/src/scheduler.rs b/crates/orchestrator/src/scheduler.rs index 93489d0..280ffaf 100644 --- a/crates/orchestrator/src/scheduler.rs +++ b/crates/orchestrator/src/scheduler.rs @@ -418,6 +418,7 @@ mod tests { .send(SchedItem { spec: make_spec("b"), ctx: make_ctx(), + original: None, }) .await; } diff --git a/deploy/podman/.env.sample b/deploy/podman/.env.sample index 525dcb1..fe2171a 100644 --- a/deploy/podman/.env.sample +++ b/deploy/podman/.env.sample @@ -57,6 +57,16 @@ ORCH_WORK_DIR=/var/lib/solstice-ci # Default points to the workspace target/runners where mise tasks may place built artifacts. RUNNER_DIR_HOST=../../target/runners +# When orchestrator runs behind NAT or in containers, set the public contact address +# that VMs can reach for gRPC log streaming (host:port). This overrides autodetection. +# Example: grpc.${ENV}.${DOMAIN}:443 (when terminated by Traefik) or a public IP:port +ORCH_CONTACT_ADDR= + +# Preferred: Provide a public base URL for runner binaries; the orchestrator will construct +# full URLs like ${SOLSTICE_RUNNER_BASE_URL}/runners/solstice-runner(-linux|-illumos) +# Example: https://runner.svc.${DOMAIN} +SOLSTICE_RUNNER_BASE_URL= + # Forge Integration secrets (set per deployment) # Shared secret used to validate Forgejo/Gitea webhooks (X-Gitea-Signature HMAC-SHA256) WEBHOOK_SECRET= diff --git a/deploy/podman/compose.yml b/deploy/podman/compose.yml index 03f8981..b8803e0 100644 --- a/deploy/podman/compose.yml +++ b/deploy/podman/compose.yml @@ -195,6 +195,10 @@ services: # Libvirt configuration for Linux/KVM LIBVIRT_URI: ${LIBVIRT_URI:-qemu:///system} LIBVIRT_NETWORK: ${LIBVIRT_NETWORK:-default} + # Public contact address for runners to stream logs to (host:port); overrides autodetection + ORCH_CONTACT_ADDR: ${ORCH_CONTACT_ADDR} + # Preferred: public base URL for runner binaries + SOLSTICE_RUNNER_BASE_URL: ${SOLSTICE_RUNNER_BASE_URL} depends_on: postgres: condition: service_healthy