greentic_runner_host/
lib.rs

1#![deny(unsafe_code)]
2//! Canonical Greentic host runtime.
3//!
4//! This crate owns tenant bindings, pack ingestion/watchers, ingress adapters,
5//! Wasmtime glue, session/state storage, and the HTTP server used by the
6//! `greentic-runner` CLI. Downstream crates embed it either through
7//! [`RunnerConfig`] + [`run`] (HTTP host) or [`HostBuilder`] (direct API access).
8
9use std::path::PathBuf;
10use std::sync::Arc;
11use std::time::Duration;
12
13use crate::secrets::SecretsBackend;
14use anyhow::{Context, Result, anyhow};
15use runner_core::env::PackConfig;
16use tokio::signal;
17
18pub mod boot;
19pub mod component_api;
20pub mod config;
21pub mod engine;
22pub mod http;
23pub mod imports;
24pub mod ingress;
25pub mod pack;
26pub mod routing;
27pub mod runner;
28pub mod runtime;
29pub mod runtime_wasmtime;
30pub mod secrets;
31pub mod storage;
32pub mod telemetry;
33pub mod verify;
34pub mod wasi;
35pub mod watcher;
36
37mod activity;
38mod host;
39pub mod oauth;
40
41pub use activity::{Activity, ActivityKind};
42pub use config::HostConfig;
43pub use host::TelemetryCfg;
44pub use host::{HostBuilder, RunnerHost, TenantHandle};
45pub use wasi::{PreopenSpec, RunnerWasiPolicy};
46
47pub use greentic_types::{EnvId, FlowId, PackId, TenantCtx, TenantId};
48
49pub use http::auth::AdminAuth;
50pub use routing::RoutingConfig;
51use routing::TenantRouting;
52pub use runner::HostServer;
53
54/// User-facing configuration for running the unified host.
55#[derive(Clone)]
56pub struct RunnerConfig {
57    pub bindings: Vec<PathBuf>,
58    pub pack: PackConfig,
59    pub port: u16,
60    pub refresh_interval: Duration,
61    pub routing: RoutingConfig,
62    pub admin: AdminAuth,
63    pub telemetry: Option<TelemetryCfg>,
64    pub secrets_backend: SecretsBackend,
65    pub wasi_policy: RunnerWasiPolicy,
66}
67
68impl RunnerConfig {
69    /// Build a [`RunnerConfig`] from environment variables and the provided binding files.
70    pub fn from_env(bindings: Vec<PathBuf>) -> Result<Self> {
71        if bindings.is_empty() {
72            anyhow::bail!("at least one bindings file is required");
73        }
74        let pack = PackConfig::from_env()?;
75        let refresh = parse_refresh_interval(std::env::var("PACK_REFRESH_INTERVAL").ok())?;
76        let port = std::env::var("PORT")
77            .ok()
78            .and_then(|value| value.parse().ok())
79            .unwrap_or(8080);
80        let routing = RoutingConfig::from_env();
81        let admin = AdminAuth::from_env();
82        let secrets_backend = SecretsBackend::from_env(std::env::var("SECRETS_BACKEND").ok())?;
83        Ok(Self {
84            bindings,
85            pack,
86            port,
87            refresh_interval: refresh,
88            routing,
89            admin,
90            telemetry: None,
91            secrets_backend,
92            wasi_policy: RunnerWasiPolicy::default(),
93        })
94    }
95
96    /// Override the HTTP port used by the host server.
97    pub fn with_port(mut self, port: u16) -> Self {
98        self.port = port;
99        self
100    }
101
102    pub fn with_wasi_policy(mut self, policy: RunnerWasiPolicy) -> Self {
103        self.wasi_policy = policy;
104        self
105    }
106}
107
108fn parse_refresh_interval(value: Option<String>) -> Result<Duration> {
109    let raw = value.unwrap_or_else(|| "30s".into());
110    humantime::parse_duration(&raw).map_err(|err| anyhow!("invalid PACK_REFRESH_INTERVAL: {err}"))
111}
112
113/// Run the unified Greentic runner host until shutdown.
114pub async fn run(cfg: RunnerConfig) -> Result<()> {
115    let RunnerConfig {
116        bindings,
117        pack,
118        port,
119        refresh_interval,
120        routing,
121        admin,
122        telemetry,
123        secrets_backend,
124        wasi_policy,
125    } = cfg;
126    #[cfg(not(feature = "telemetry"))]
127    let _ = telemetry;
128
129    let mut builder = HostBuilder::new();
130    for path in &bindings {
131        let host_config = HostConfig::load_from_path(path)
132            .with_context(|| format!("failed to load host bindings {}", path.display()))?;
133        builder = builder.with_config(host_config);
134    }
135    #[cfg(feature = "telemetry")]
136    if let Some(telemetry) = telemetry.clone() {
137        builder = builder.with_telemetry(telemetry);
138    }
139    builder = builder
140        .with_wasi_policy(wasi_policy.clone())
141        .with_secrets_manager(
142            secrets_backend
143                .build_manager()
144                .context("failed to initialise secrets backend")?,
145        );
146
147    let host = Arc::new(builder.build()?);
148    host.start().await?;
149
150    let (watcher, reload_handle) =
151        watcher::start_pack_watcher(Arc::clone(&host), pack.clone(), refresh_interval).await?;
152
153    let routing = TenantRouting::new(routing.clone());
154    let server = HostServer::new(
155        port,
156        host.active_packs(),
157        routing,
158        host.health_state(),
159        Some(reload_handle),
160        admin.clone(),
161    )?;
162
163    tokio::select! {
164        result = server.serve() => {
165            result?;
166        }
167        _ = signal::ctrl_c() => {
168            tracing::info!("received shutdown signal");
169        }
170    }
171
172    drop(watcher);
173    host.stop().await?;
174    Ok(())
175}